####################################################################
#                                                                  #
# THIS FILE IS PART OF THE pycollada LIBRARY SOURCE CODE.          #
# USE, DISTRIBUTION AND REPRODUCTION OF THIS LIBRARY SOURCE IS     #
# GOVERNED BY A BSD-STYLE SOURCE LICENSE INCLUDED WITH THIS SOURCE #
# IN 'COPYING'. PLEASE READ THESE TERMS BEFORE DISTRIBUTING.       #
#                                                                  #
# THE pycollada SOURCE CODE IS (C) COPYRIGHT 2011                  #
# by Jeff Terrace and contributors                                 #
#                                                                  #
####################################################################

"""This module contains several classes related to the scene graph.

Supported scene nodes are:
  * <node> which is loaded as a Node
  * <instance_camera> which is loaded as a CameraNode
  * <instance_light> which is loaded as a LightNode
  * <instance_material> which is loaded as a MaterialNode
  * <instance_geometry> which is loaded as a GeometryNode
  * <instance_controller> which is loaded as a ControllerNode
  * <scene> which is loaded as a Scene

"""

import copy
import numpy

from collada.common import DaeObject, E, tag
from collada.common import DaeError, DaeIncompleteError, DaeBrokenRefError, \
        DaeMalformedError, DaeUnsupportedError
from collada.util import toUnitVec
from collada.xmlutil import etree as ElementTree


class DaeInstanceNotLoadedError(Exception):
    """Raised when an instance_node refers to a node that isn't loaded yet. Will always be caught"""
    def __init__(self, msg):
        super(DaeInstanceNotLoadedError,self).__init__()
        self.msg = msg


class SceneNode(DaeObject):
    """Abstract base class for all nodes within a scene."""

    def objects(self, tipo, matrix=None):
        """Iterate through all objects under this node that match `tipo`.
        The objects will be bound and transformed via the scene transformations.

        :param str tipo:
          A string for the desired object type. This can be one of 'geometry',
          'camera', 'light', or 'controller'.
        :param numpy.matrix matrix:
          An optional transformation matrix

        :rtype: generator that yields the type specified

        """
        pass


def makeRotationMatrix(x, y, z, angle):
    """Build and return a transform 4x4 matrix to rotate `angle` radians
    around (`x`,`y`,`z`) axis."""
    c = numpy.cos(angle)
    s = numpy.sin(angle)
    t = (1-c)
    return numpy.array([[t*x*x+c,     t*x*y - s*z, t*x*z + s*y, 0],
                        [t*x*y+s*z,   t*y*y + c,   t*y*z - s*x, 0],
                        [t*x*z - s*y, t*y*z + s*x, t*z*z + c,   0],
                        [0,           0,           0,           1]],
                       dtype=numpy.float32 )


class Transform(DaeObject):
    """Base class for all transformation types"""

    def save(self):
        pass


class TranslateTransform(Transform):
    """Contains a translation transformation as defined in the collada <translate> tag."""

    def __init__(self, x, y, z, xmlnode=None):
        """Creates a translation transformation

        :param float x:
          x coordinate
        :param float y:
          y coordinate
        :param float z:
          z coordinate
        :param xmlnode:
           When loaded, the xmlnode it comes from

        """
        self.x = x
        """x coordinate"""
        self.y = y
        """y coordinate"""
        self.z = z
        """z coordinate"""
        self.matrix = numpy.identity(4, dtype=numpy.float32)
        """The resulting transformation matrix. This will be a numpy.array of size 4x4."""
        self.matrix[:3,3] = [ x, y, z ]
        self.xmlnode = xmlnode
        """ElementTree representation of the transform."""
        if xmlnode is None:
            self.xmlnode = E.translate(' '.join([str(x),str(y),str(z)]))

    @staticmethod
    def load(collada, node):
        floats = numpy.fromstring(node.text, dtype=numpy.float32, sep=' ')
        if len(floats) != 3:
            raise DaeMalformedError("Translate node requires three float values")
        return TranslateTransform(floats[0], floats[1], floats[2], node)

    def __str__(self):
        return '<TranslateTransform (%s, %s, %s)>' % (self.x, self.y, self.z)

    def __repr__(self):
        return str(self)


class RotateTransform(Transform):
    """Contains a rotation transformation as defined in the collada <rotate> tag."""

    def __init__(self, x, y, z, angle, xmlnode=None):
        """Creates a rotation transformation

        :param float x:
          x coordinate
        :param float y:
          y coordinate
        :param float z:
          z coordinate
        :param float angle:
          angle of rotation, in radians
        :param xmlnode:
           When loaded, the xmlnode it comes from

        """
        self.x = x
        """x coordinate"""
        self.y = y
        """y coordinate"""
        self.z = z
        """z coordinate"""
        self.angle = angle
        """angle of rotation, in radians"""
        self.matrix = makeRotationMatrix(x, y, z, angle*numpy.pi/180.0)
        """The resulting transformation matrix. This will be a numpy.array of size 4x4."""
        self.xmlnode = xmlnode
        """ElementTree representation of the transform."""
        if xmlnode is None:
            self.xmlnode = E.rotate(' '.join([str(x),str(y),str(z),str(angle)]))

    @staticmethod
    def load(collada, node):
        floats = numpy.fromstring(node.text, dtype=numpy.float32, sep=' ')
        if len(floats) != 4:
            raise DaeMalformedError("Rotate node requires four float values")
        return RotateTransform(floats[0], floats[1], floats[2], floats[3], node)

    def __str__(self):
        return '<RotateTransform (%s, %s, %s) angle=%s>' % (self.x, self.y, self.z, self.angle)

    def __repr__(self):
        return str(self)


class ScaleTransform(Transform):
    """Contains a scale transformation as defined in the collada <scale> tag."""

    def __init__(self, x, y, z, xmlnode=None):
        """Creates a scale transformation

        :param float x:
          x coordinate
        :param float y:
          y coordinate
        :param float z:
          z coordinate
        :param xmlnode:
           When loaded, the xmlnode it comes from

        """
        self.x = x
        """x coordinate"""
        self.y = y
        """y coordinate"""
        self.z = z
        """z coordinate"""
        self.matrix = numpy.identity(4, dtype=numpy.float32)
        """The resulting transformation matrix. This will be a numpy.array of size 4x4."""
        self.matrix[0,0] = x
        self.matrix[1,1] = y
        self.matrix[2,2] = z
        self.xmlnode = xmlnode
        """ElementTree representation of the transform."""
        if xmlnode is None:
            self.xmlnode = E.scale(' '.join([str(x),str(y),str(z)]))

    @staticmethod
    def load(collada, node):
        floats = numpy.fromstring(node.text, dtype=numpy.float32, sep=' ')
        if len(floats) != 3:
            raise DaeMalformedError("Scale node requires three float values")
        return ScaleTransform(floats[0], floats[1], floats[2], node)

    def __str__(self):
        return '<ScaleTransform (%s, %s, %s)>' % (self.x, self.y, self.z)

    def __repr__(self):
        return str(self)


class MatrixTransform(Transform):
    """Contains a matrix transformation as defined in the collada <matrix> tag."""

    def __init__(self, matrix, xmlnode=None):
        """Creates a matrix transformation

        :param numpy.array matrix:
          This should be an unshaped numpy array of floats of length 16
        :param xmlnode:
           When loaded, the xmlnode it comes from

        """
        self.matrix = matrix
        """The resulting transformation matrix. This will be a numpy.array of size 4x4."""
        if len(self.matrix) != 16: raise DaeMalformedError('Corrupted matrix transformation node')
        self.matrix.shape = (4, 4)
        self.xmlnode = xmlnode
        """ElementTree representation of the transform."""
        if xmlnode is None:
            self.xmlnode = E.matrix(' '.join(map(str, self.matrix.flat)))

    @staticmethod
    def load(collada, node):
        floats = numpy.fromstring(node.text, dtype=numpy.float32, sep=' ')
        return MatrixTransform(floats, node)

    def __str__(self):
        return '<MatrixTransform>'

    def __repr__(self):
        return str(self)


class LookAtTransform(Transform):
    """Contains a transformation for aiming a camera as defined in the collada <lookat> tag."""

    def __init__(self, eye, interest, upvector, xmlnode=None):
        """Creates a lookat transformation

        :param numpy.array eye:
          An unshaped numpy array of floats of length 3 containing the position of the eye
        :param numpy.array interest:
          An unshaped numpy array of floats of length 3 containing the point of interest
        :param numpy.array upvector:
          An unshaped numpy array of floats of length 3 containing the up-axis direction
        :param xmlnode:
          When loaded, the xmlnode it comes from

        """
        self.eye = eye
        """A numpy array of length 3 containing the position of the eye"""
        self.interest = interest
        """A numpy array of length 3 containing the point of interest"""
        self.upvector = upvector
        """A numpy array of length 3 containing the up-axis direction"""

        if len(eye) != 3 or len(interest) != 3 or len(upvector) != 3:
            raise DaeMalformedError('Corrupted lookat transformation node')

        self.matrix = numpy.identity(4, dtype=numpy.float32)
        """The resulting transformation matrix. This will be a numpy.array of size 4x4."""

        front = toUnitVec(numpy.subtract(eye,interest))
        side = numpy.multiply(-1, toUnitVec(numpy.cross(front, upvector)))
        self.matrix[0,0:3] = side
        self.matrix[1,0:3] = upvector
        self.matrix[2,0:3] = front
        self.matrix[3,0:3] = eye

        self.xmlnode = xmlnode
        """ElementTree representation of the transform."""
        if xmlnode is None:
            self.xmlnode = E.lookat(' '.join(map(str,
                                        numpy.concatenate((self.eye, self.interest, self.upvector)) )))
    @staticmethod
    def load(collada, node):
        floats = numpy.fromstring(node.text, dtype=numpy.float32, sep=' ')
        if len(floats) != 9:
            raise DaeMalformedError("Lookat node requires 9 float values")
        return LookAtTransform(floats[0:3], floats[3:6], floats[6:9], node)

    def __str__(self):
        return '<LookAtTransform>'

    def __repr__(self):
        return str(self)


class Node(SceneNode):
    """Represents a node object, which is a point on the scene graph, as defined in the collada <node> tag.

    Contains the list of transformations effecting the node as well as any children.
    """

    def __init__(self, id, children=None, transforms=None, xmlnode=None):
        """Create a node in the scene graph.

        :param str id:
          A unique string identifier for the node
        :param list children:
          A list of child nodes of this node. This can contain any
          object that inherits from :class:`collada.scene.SceneNode`
        :param list transforms:
          A list of transformations effecting the node. This can
          contain any object that inherits from :class:`collada.scene.Transform`
        :param xmlnode:
          When loaded, the xmlnode it comes from

        """
        self.id = id
        """The unique string identifier for the node"""
        self.children = []
        """A list of child nodes of this node. This can contain any
          object that inherits from :class:`collada.scene.SceneNode`"""
        if children is not None:
            self.children = children
        self.transforms = []
        if transforms is not None:
            self.transforms = transforms
        """A list of transformations effecting the node. This can
          contain any object that inherits from :class:`collada.scene.Transform`"""
        self.matrix = numpy.identity(4, dtype=numpy.float32)
        """A numpy.array of size 4x4 containing a transformation matrix that
        combines all the transformations in :attr:`transforms`. This will only
        be updated after calling :meth:`save`."""

        for t in self.transforms:
            self.matrix = numpy.dot(self.matrix, t.matrix)

        if xmlnode is not None:
            self.xmlnode = xmlnode
            """ElementTree representation of the transform."""
        else:
            self.xmlnode = E.node(id=self.id, name=self.id)
            for t in self.transforms:
                self.xmlnode.append(t.xmlnode)
            for c in self.children:
                self.xmlnode.append(c.xmlnode)

    def objects(self, tipo, matrix=None):
        """Iterate through all objects under this node that match `tipo`.
        The objects will be bound and transformed via the scene transformations.

        :param str tipo:
          A string for the desired object type. This can be one of 'geometry',
          'camera', 'light', or 'controller'.
        :param numpy.matrix matrix:
          An optional transformation matrix

        :rtype: generator that yields the type specified

        """
        if not matrix is None: M = numpy.dot( matrix, self.matrix )
        else: M = self.matrix
        for node in self.children:
            for obj in node.objects(tipo, M):
                yield obj

    def save(self):
        """Saves the geometry back to :attr:`xmlnode`. Also updates
        :attr:`matrix` if :attr:`transforms` has been modified."""
        self.matrix = numpy.identity(4, dtype=numpy.float32)
        for t in self.transforms:
            self.matrix = numpy.dot(self.matrix, t.matrix)

        for child in self.children:
            child.save()

        if self.id is not None:
            self.xmlnode.set('id', self.id)
            self.xmlnode.set('name', self.id)
        for t in self.transforms:
            if t.xmlnode not in self.xmlnode:
                self.xmlnode.append(t.xmlnode)
        for c in self.children:
            if c.xmlnode not in self.xmlnode:
                self.xmlnode.append(c.xmlnode)
        xmlnodes = [c.xmlnode for c in self.children]
        xmlnodes.extend([t.xmlnode for t in self.transforms])
        for n in self.xmlnode:
            if n not in xmlnodes:
                self.xmlnode.remove(n)

    @staticmethod
    def load( collada, node, localscope ):
        id = node.get('id')
        children = []
        transforms = []

        for subnode in node:
            try:
                n = loadNode(collada, subnode, localscope)
                if isinstance(n, Transform):
                    transforms.append(n)
                elif n is not None:
                    children.append(n)
            except DaeError as ex:
                collada.handleError(ex)

        return Node(id, children, transforms, xmlnode=node)

    def __str__(self):
        return '<Node transforms=%d, children=%d>' % (len(self.transforms), len(self.children))

    def __repr__(self):
        return str(self)


class NodeNode(Node):
    """Represents a node being instantiated in a scene, as defined in the collada <instande_node> tag."""

    def __init__(self, node, xmlnode=None):
        """Creates a node node

        :param collada.scene.Node node:
          A node to instantiate in the scene
        :param xmlnode:
          When loaded, the xmlnode it comes from

        """
        self.node = node
        """An object of type :class:`collada.scene.Node` representing the node to bind in the scene"""

        if xmlnode != None:
            self.xmlnode = xmlnode
            """ElementTree representation of the node node."""
        else:
            self.xmlnode = E.instance_node(url="#%s" % self.node.id)

    def objects(self, tipo, matrix=None):
        for obj in self.node.objects(tipo, matrix):
            yield obj

    id = property(lambda s: s.node.id)
    children = property(lambda s: s.node.children)
    matrix = property(lambda s: s.node.matrix)

    @staticmethod
    def load( collada, node, localscope ):
        url = node.get('url')
        if not url.startswith('#'):
            raise DaeMalformedError('Invalid url in node instance %s' % url)
        referred_node = localscope.get(url[1:])
        if not referred_node:
            referred_node = collada.nodes.get(url[1:])
        if not referred_node:
            raise DaeInstanceNotLoadedError('Node %s not found in library'%url)
        return NodeNode(referred_node, xmlnode=node)

    def save(self):
        """Saves the node node back to :attr:`xmlnode`"""
        self.xmlnode.set('url', "#%s" % self.node.id)

    def __str__(self):
        return '<NodeNode node=%s>' % (self.node.id,)

    def __repr__(self):
        return str(self)


class GeometryNode(SceneNode):
    """Represents a geometry instance in a scene, as defined in the collada <instance_geometry> tag."""

    def __init__(self, geometry, materials=None, xmlnode=None):
        """Creates a geometry node

        :param collada.geometry.Geometry geometry:
          A geometry to instantiate in the scene
        :param list materials:
          A list containing items of type :class:`collada.scene.MaterialNode`.
          Each of these represents a material that the geometry should be
          bound to.
        :param xmlnode:
          When loaded, the xmlnode it comes from

        """
        self.geometry = geometry
        """An object of type :class:`collada.geometry.Geometry` representing the
        geometry to bind in the scene"""
        self.materials = []
        """A list containing items of type :class:`collada.scene.MaterialNode`.
          Each of these represents a material that the geometry is bound to."""
        if materials is not None:
            self.materials = materials
        if xmlnode != None:
            self.xmlnode = xmlnode
            """ElementTree representation of the geometry node."""
        else:
            self.xmlnode = E.instance_geometry(url="#%s" % self.geometry.id)
            if len(self.materials) > 0:
                self.xmlnode.append(E.bind_material(
                    E.technique_common(
                        *[mat.xmlnode for mat in self.materials]
                    )
                ))

    def objects(self, tipo, matrix=None):
        """Yields a :class:`collada.geometry.BoundGeometry` if ``tipo=='geometry'``"""
        if tipo == 'geometry':
            if matrix is None: matrix = numpy.identity(4, dtype=numpy.float32)
            materialnodesbysymbol = {}
            for mat in self.materials:
                materialnodesbysymbol[mat.symbol] = mat
            yield self.geometry.bind(matrix, materialnodesbysymbol)

    @staticmethod
    def load( collada, node ):
        url = node.get('url')
        if not url.startswith('#'): raise DaeMalformedError('Invalid url in geometry instance %s' % url)
        geometry = collada.geometries.get(url[1:])
        if not geometry: raise DaeBrokenRefError('Geometry %s not found in library'%url)
        matnodes = node.findall('%s/%s/%s'%( collada.tag('bind_material'), collada.tag('technique_common'), collada.tag('instance_material') ) )
        materials = []
        for matnode in matnodes:
            materials.append( MaterialNode.load(collada, matnode) )
        return GeometryNode( geometry, materials, xmlnode=node)

    def save(self):
        """Saves the geometry node back to :attr:`xmlnode`"""
        self.xmlnode.set('url', "#%s" % self.geometry.id)

        for m in self.materials:
            m.save()

        matparent = self.xmlnode.find('%s/%s'%( tag('bind_material'), tag('technique_common') ) )
        if matparent is None and len(self.materials)==0:
            return
        elif matparent is None:
            matparent = E.technique_common()
            self.xmlnode.append(E.bind_material(matparent))
        elif len(self.materials) == 0 and matparent is not None:
            bindnode = self.xmlnode.find('%s' % tag('bind_material'))
            self.xmlnode.remove(bindnode)
            return

        for m in self.materials:
            if m.xmlnode not in matparent:
                matparent.append(m.xmlnode)
        xmlnodes = [m.xmlnode for m in self.materials]
        for n in matparent:
            if n not in xmlnodes:
                matparent.remove(n)

    def __str__(self):
        return '<GeometryNode geometry=%s>' % (self.geometry.id,)

    def __repr__(self):
        return str(self)


class ControllerNode(SceneNode):
    """Represents a controller instance in a scene, as defined in the collada <instance_controller> tag. **This class is highly
    experimental. More support will be added in version 0.4.**"""

    def __init__(self, controller, materials, xmlnode=None):
        """Creates a controller node

        :param collada.controller.Controller controller:
          A controller to instantiate in the scene
        :param list materials:
          A list containing items of type :class:`collada.scene.MaterialNode`.
          Each of these represents a material that the controller should be
          bound to.
        :param xmlnode:
          When loaded, the xmlnode it comes from

        """
        self.controller = controller
        """ An object of type :class:`collada.controller.Controller` representing
        the controller being instantiated in the scene"""
        self.materials = materials
        """A list containing items of type :class:`collada.scene.MaterialNode`.
          Each of these represents a material that the controller is bound to."""
        if xmlnode != None:
            self.xmlnode = xmlnode
            """ElementTree representation of the controller node."""
        else:
            self.xmlnode = ElementTree.Element( collada.tag('instance_controller') )
            bindnode = ElementTree.Element( collada.tag('bind_material') )
            technode = ElementTree.Element( collada.tag('technique_common') )
            bindnode.append( technode )
            self.xmlnode.append( bindnode )
            for mat in materials: technode.append( mat.xmlnode )

    def objects(self, tipo, matrix=None):
        """Yields a :class:`collada.controller.BoundController` if ``tipo=='controller'``"""
        if tipo == 'controller':
            if matrix is None: matrix = numpy.identity(4, dtype=numpy.float32)
            materialnodesbysymbol = {}
            for mat in self.materials:
                materialnodesbysymbol[mat.symbol] = mat
            yield self.controller.bind(matrix, materialnodesbysymbol)

    @staticmethod
    def load( collada, node ):
        url = node.get('url')
        if not url.startswith('#'): raise DaeMalformedError('Invalid url in controller instance %s' % url)
        controller = collada.controllers.get(url[1:])
        if not controller: raise DaeBrokenRefError('Controller %s not found in library'%url)
        matnodes = node.findall('%s/%s/%s'%( collada.tag('bind_material'), collada.tag('technique_common'), collada.tag('instance_material') ) )
        materials = []
        for matnode in matnodes:
            materials.append( MaterialNode.load(collada, matnode) )
        return ControllerNode( controller, materials, xmlnode=node)

    def save(self):
        """Saves the controller node back to :attr:`xmlnode`"""
        self.xmlnode.set('url', '#'+self.controller.id)
        for mat in self.materials:
            mat.save()

    def __str__(self):
        return '<ControllerNode controller=%s>' % (self.controller.id,)

    def __repr__(self):
        return str(self)


class MaterialNode(SceneNode):
    """Represents a material being instantiated in a scene, as defined in the collada <instance_material> tag."""

    def __init__(self, symbol, target, inputs, xmlnode = None):
        """Creates a material node

        :param str symbol:
          The symbol within a geometry this material should be bound to
        :param collada.material.Material target:
          The material object being bound to
        :param list inputs:
          A list of tuples of the form ``(semantic, input_semantic, set)`` mapping
          texcoords or other inputs to material input channels, e.g.
          ``('TEX0', 'TEXCOORD', '0')`` would map the effect parameter ``'TEX0'``
          to the ``'TEXCOORD'`` semantic of the geometry, using texture coordinate
          set ``0``.
        :param xmlnode:
          When loaded, the xmlnode it comes from

        """
        self.symbol = symbol
        """The symbol within a geometry this material should be bound to"""
        self.target = target
        """An object of type :class:`collada.material.Material` representing the material object being bound to"""
        self.inputs = inputs
        """A list of tuples of the form ``(semantic, input_semantic, set)`` mapping
          texcoords or other inputs to material input channels, e.g.
          ``('TEX0', 'TEXCOORD', '0')`` would map the effect parameter ``'TEX0'``
          to the ``'TEXCOORD'`` semantic of the geometry, using texture coordinate
          set ``0``."""
        if xmlnode is not None:
            self.xmlnode = xmlnode
            """ElementTree representation of the material node."""
        else:
            self.xmlnode = E.instance_material(
                *[E.bind_vertex_input(semantic=sem, input_semantic=input_sem, input_set=set)
                  for sem, input_sem, set in self.inputs]
            , **{'symbol': self.symbol, 'target':"#%s"%self.target.id} )

    @staticmethod
    def load(collada, node):
        inputs = []
        for inputnode in node.findall( collada.tag('bind_vertex_input') ):
            inputs.append( ( inputnode.get('semantic'), inputnode.get('input_semantic'), inputnode.get('input_set') ) )
        targetid = node.get('target')
        if not targetid.startswith('#'): raise DaeMalformedError('Incorrect target id in material '+targetid)
        target = collada.materials.get(targetid[1:])
        if not target: raise DaeBrokenRefError('Material %s not found'%targetid)
        return MaterialNode(node.get('symbol'), target, inputs, xmlnode = node)

    def objects(self):
        pass

    def save(self):
        """Saves the material node back to :attr:`xmlnode`"""
        self.xmlnode.set('symbol', self.symbol)
        self.xmlnode.set('target', "#%s"%self.target.id)

        inputs_in = []
        for i in self.xmlnode.findall( tag('bind_vertex_input') ):
            input_tuple = ( i.get('semantic'), i.get('input_semantic'), i.get('input_set') )
            if input_tuple not in self.inputs:
                self.xmlnode.remove(i)
            else:
                inputs_in.append(input_tuple)
        for i in self.inputs:
            if i not in inputs_in:
                self.xmlnode.append(E.bind_vertex_input(semantic=i[0], input_semantic=i[1], input_set=i[2]))

    def __str__(self):
        return '<MaterialNode symbol=%s targetid=%s>' % (self.symbol, self.target.id)

    def __repr__(self):
        return str(self)


class CameraNode(SceneNode):
    """Represents a camera being instantiated in a scene, as defined in the collada <instance_camera> tag."""

    def __init__(self, camera, xmlnode=None):
        """Create a camera instance

        :param collada.camera.Camera camera:
          The camera being instantiated
        :param xmlnode:
          When loaded, the xmlnode it comes from

        """
        self.camera = camera
        """An object of type :class:`collada.camera.Camera` representing the instantiated camera"""
        if xmlnode != None:
            self.xmlnode = xmlnode
            """ElementTree representation of the camera node."""
        else:
            self.xmlnode = E.instance_camera(url="#%s"%camera.id)

    def objects(self, tipo, matrix=None):
        """Yields a :class:`collada.camera.BoundCamera` if ``tipo=='camera'``"""
        if tipo == 'camera':
            if matrix is None: matrix = numpy.identity(4, dtype=numpy.float32)
            yield self.camera.bind(matrix)

    @staticmethod
    def load( collada, node ):
        url = node.get('url')
        if not url.startswith('#'): raise DaeMalformedError('Invalid url in camera instance %s' % url)
        camera = collada.cameras.get(url[1:])
        if not camera: raise DaeBrokenRefError('Camera %s not found in library'%url)
        return CameraNode( camera, xmlnode=node)

    def save(self):
        """Saves the camera node back to :attr:`xmlnode`"""
        self.xmlnode.set('url', '#'+self.camera.id)

    def __str__(self):
        return '<CameraNode camera=%s>' % (self.camera.id,)

    def __repr__(self):
        return str(self)


class LightNode(SceneNode):
    """Represents a light being instantiated in a scene, as defined in the collada <instance_light> tag."""

    def __init__(self, light, xmlnode=None):
        """Create a light instance

        :param collada.light.Light light:
          The light being instantiated
        :param xmlnode:
          When loaded, the xmlnode it comes from

        """
        self.light = light
        """An object of type :class:`collada.light.Light` representing the instantiated light"""
        if xmlnode != None:
            self.xmlnode = xmlnode
            """ElementTree representation of the light node."""
        else:
            self.xmlnode = E.instance_light(url="#%s"%light.id)

    def objects(self, tipo, matrix=None):
        """Yields a :class:`collada.light.BoundLight` if ``tipo=='light'``"""
        if tipo == 'light':
            if matrix is None: matrix = numpy.identity(4, dtype=numpy.float32)
            yield self.light.bind(matrix)

    @staticmethod
    def load( collada, node ):
        url = node.get('url')
        if not url.startswith('#'): raise DaeMalformedError('Invalid url in light instance %s' % url)
        light = collada.lights.get(url[1:])
        if not light: raise DaeBrokenRefError('Light %s not found in library'%url)
        return LightNode( light, xmlnode=node)

    def save(self):
        """Saves the light node back to :attr:`xmlnode`"""
        self.xmlnode.set('url', '#'+self.light.id)

    def __str__(self): return '<LightNode light=%s>' % (self.light.id,)
    def __repr__(self): return str(self)


class ExtraNode(SceneNode):
    """Represents extra information in a scene, as defined in a collada <extra> tag."""

    def __init__(self, xmlnode):
        """Create an extra node which stores arbitrary xml

        :param xmlnode:
          Should be an ElementTree instance of tag type <extra>

        """
        if xmlnode != None:
            self.xmlnode = xmlnode
            """ElementTree representation of the extra node."""
        else:
            self.xmlnode = E.extra()

    def objects(self, tipo, matrix=None):
        if tipo == 'extra':
            for e in self.xmlnode.findall(tag(tipo)):
                yield e

    @staticmethod
    def load( collada, node ):
        return ExtraNode(node)

    def save(self):
        pass


def loadNode( collada, node, localscope ):
    """Generic scene node loading from an xml `node` and a `collada` object.

    Knowing the supported nodes, create the appropriate class for the given node
    and return it.

    """
    if node.tag == collada.tag('node'): return Node.load(collada, node, localscope)
    elif node.tag == collada.tag('translate'): return TranslateTransform.load(collada, node)
    elif node.tag == collada.tag('rotate'): return RotateTransform.load(collada, node)
    elif node.tag == collada.tag('scale'): return ScaleTransform.load(collada, node)
    elif node.tag == collada.tag('matrix'): return MatrixTransform.load(collada, node)
    elif node.tag == collada.tag('lookat'): return LookAtTransform.load(collada, node)
    elif node.tag == collada.tag('instance_geometry'): return GeometryNode.load(collada, node)
    elif node.tag == collada.tag('instance_camera'): return CameraNode.load(collada, node)
    elif node.tag == collada.tag('instance_light'): return LightNode.load(collada, node)
    elif node.tag == collada.tag('instance_controller'): return ControllerNode.load(collada, node)
    elif node.tag == collada.tag('instance_node'): return NodeNode.load(collada, node, localscope)
    elif node.tag == collada.tag('extra'):
        return ExtraNode.load(collada, node)
    elif node.tag == collada.tag('asset'):
        return None
    else: raise DaeUnsupportedError('Unknown scene node %s' % str(node.tag))


class Scene(DaeObject):
    """The root object for a scene, as defined in a collada <scene> tag"""

    def __init__(self, id, nodes, xmlnode=None, collada=None):
        """Create a scene

        :param str id:
          A unique string identifier for the scene
        :param list nodes:
          A list of type :class:`collada.scene.Node` representing the nodes in the scene
        :param xmlnode:
          When loaded, the xmlnode it comes from
        :param collada:
          The collada instance this is part of

        """
        self.id = id
        """The unique string identifier for the scene"""
        self.nodes = nodes
        """A list of type :class:`collada.scene.Node` representing the nodes in the scene"""
        self.collada = collada
        """The collada instance this is part of"""
        if xmlnode != None:
            self.xmlnode = xmlnode
            """ElementTree representation of the scene node."""
        else:
            self.xmlnode = E.visual_scene(id=self.id)
            for node in nodes:
                self.xmlnode.append( node.xmlnode )

    def objects(self, tipo):
        """Iterate through all objects in the scene that match `tipo`.
        The objects will be bound and transformed via the scene transformations.

        :param str tipo:
          A string for the desired object type. This can be one of 'geometry',
          'camera', 'light', or 'controller'.

        :rtype: generator that yields the type specified

        """
        matrix = None
        for node in self.nodes:
            for obj in node.objects(tipo, matrix): yield obj

    @staticmethod
    def load( collada, node ):
        id = node.get('id')
        nodes = []
        tried_loading = []
        succeeded = False
        localscope = {}
        for nodenode in node.findall(collada.tag('node')):
            try:
                N = loadNode(collada, nodenode, localscope)
            except DaeInstanceNotLoadedError as ex:
                tried_loading.append((nodenode, ex))
            except DaeError as ex:
                collada.handleError(ex)
            else:
                if N is not None:
                    nodes.append( N )
                    if N.id and N.id not in localscope:
                        localscope[N.id] = N
                    succeeded = True
        while len(tried_loading) > 0 and succeeded:
            succeeded = False
            next_tried = []
            for nodenode, ex in tried_loading:
                try:
                    N = loadNode(collada, nodenode, localscope)
                except DaeInstanceNotLoadedError as ex:
                    next_tried.append((nodenode, ex))
                except DaeError as ex:
                    collada.handleError(ex)
                else:
                    if N is not None:
                        nodes.append( N )
                        succeeded = True
            tried_loading = next_tried
        if len(tried_loading) > 0:
            for nodenode, ex in tried_loading:
                raise DaeBrokenRefError(ex.msg)

        return Scene(id, nodes, xmlnode=node, collada=collada)

    def save(self):
        """Saves the scene back to :attr:`xmlnode`"""
        self.xmlnode.set('id', self.id)
        for node in self.nodes:
            node.save()
            if node.xmlnode not in self.xmlnode:
                self.xmlnode.append(node.xmlnode)
        xmlnodes = [n.xmlnode for n in self.nodes]
        for node in self.xmlnode:
            if node not in xmlnodes:
                self.xmlnode.remove(node)

    def __str__(self):
        return '<Scene id=%s nodes=%d>' % (self.id, len(self.nodes))

    def __repr__(self):
        return str(self)

