package aerys.minko.scene.controller
{
    import aerys.minko.ns.minko_math;
    import aerys.minko.render.Viewport;
    import aerys.minko.scene.node.Group;
    import aerys.minko.scene.node.ISceneNode;
    import aerys.minko.scene.node.Scene;
    import aerys.minko.type.math.Matrix4x4;

    import flash.display.BitmapData;
    import flash.utils.Dictionary;

    import mx.olap.aggregators.MaxAggregator;

    use namespace minko_math;

    /**
     * The TransformController handles the batched update of all the local to world matrices
     * of a sub-scene. As such, it will only be active on the root node of a sub-scene and will
     * automatically disable itself on other nodes.
     *
     * @author Jean-Marc Le Roux
     *
     */
    public final class TransformController extends AbstractController
    {

        private static const FLAG_NONE                      : uint  = 0;
        private static const FLAG_INIT_LOCAL_TO_WORLD       : uint  = 1;
        private static const FLAG_INIT_WORLD_TO_LOCAL       : uint  = 2;
        private static const FLAG_SYNCHRONIZE_TRANSFORMS    : uint  = 4;
        private static const FLAG_LOCK_TRANSFORMS           : uint  = 8;

        private var _target					: ISceneNode;

        private var _invalidList			: Boolean;

        private var _nodeToId				: Dictionary;
        private var _idToNode				: Vector.<ISceneNode>
        private var _transforms				: Vector.<Matrix4x4>;
        private var _flags	                : Vector.<uint>;
        private var _localToWorldTransforms : Vector.<Matrix4x4>;
        private var _worldToLocalTransforms : Vector.<Matrix4x4>;
        private var _numChildren			: Vector.<uint>;
        private var _firstChildId			: Vector.<uint>;
        private var _parentId				: Vector.<int>;

        public function TransformController()
        {
            super();

            targetAdded.add(targetAddedHandler);
            targetRemoved.add(targetRemovedHandler);
        }

        private function renderingBeginHandler(scene			: Scene,
                                               viewport		: Viewport,
                                               destination	: BitmapData,
                                               time			: Number) : void
        {
            if (_invalidList)
                updateTransformsList();

            if (_transforms.length)
                updateLocalToWorld();
        }

        private function updateRootLocalToWorld(nodeId : uint = 0) : void
        {
            var rootLocalToWorld	: Matrix4x4		= _localToWorldTransforms[nodeId];
            var rootTransform		: Matrix4x4		= _transforms[nodeId];
            var root				: ISceneNode	= _idToNode[nodeId];
            var rootFlags           : uint          = _flags[nodeId];

            if (rootTransform._hasChanged || !(rootFlags & FLAG_INIT_LOCAL_TO_WORLD))
            {
                if (rootFlags & FLAG_LOCK_TRANSFORMS)
                    rootLocalToWorld.lock();

                rootLocalToWorld.copyFrom(rootTransform);

                if (nodeId != 0)
                    rootLocalToWorld.append(_localToWorldTransforms[_parentId[nodeId]]);

                if (rootFlags & FLAG_LOCK_TRANSFORMS)
                    rootLocalToWorld.unlock();

                rootTransform._hasChanged = false;
                _flags[nodeId] |= FLAG_INIT_LOCAL_TO_WORLD;
                root.localToWorldTransformChanged.execute(root, rootLocalToWorld);

                if (rootFlags & FLAG_SYNCHRONIZE_TRANSFORMS)
                {
                    var rootWorldToLocal : Matrix4x4 = _worldToLocalTransforms[nodeId]
                        || (_worldToLocalTransforms[nodeId] = new Matrix4x4());

                    if (rootFlags & FLAG_LOCK_TRANSFORMS)
                        rootWorldToLocal.lock();

                    rootWorldToLocal.copyFrom(rootLocalToWorld).invert();

                    if (rootFlags & FLAG_LOCK_TRANSFORMS)
                        rootWorldToLocal.unlock();
                }
            }
        }

        private function updateLocalToWorld(nodeId : uint = 0, subtreeOnly : Boolean = false) : void
        {
            var numNodes 			: uint 			= _transforms.length;
            var subtreeMax          : uint          = nodeId;

            updateRootLocalToWorld(nodeId);

            while (nodeId < numNodes)
            {
                var localToWorld 	: Matrix4x4	= _localToWorldTransforms[nodeId];
                var numChildren		: uint		= _numChildren[nodeId];
                var firstChildId	: uint		= _firstChildId[nodeId];
                var lastChildId		: uint		= firstChildId + numChildren;
                var isDirty			: Boolean	= localToWorld._hasChanged;

                localToWorld._hasChanged = false;

                if (lastChildId > subtreeMax)
                    subtreeMax = lastChildId;

                for (var childId : uint = firstChildId; childId < lastChildId; ++childId)
                {
                    var childTransform		: Matrix4x4		= _transforms[childId];
                    var childLocalToWorld	: Matrix4x4		= _localToWorldTransforms[childId];
                    var childFlags          : uint          = _flags[childId];
                    var childIsDirty		: Boolean		= isDirty || childTransform._hasChanged
                        || !(childFlags & FLAG_INIT_LOCAL_TO_WORLD);

                    if (childIsDirty)
                    {
                        var child	: ISceneNode	= _idToNode[childId];

                        if (childFlags & FLAG_LOCK_TRANSFORMS)
                            childLocalToWorld.lock();

                        childLocalToWorld
                            .copyFrom(childTransform)
                            .append(localToWorld);

                        if (childFlags & FLAG_LOCK_TRANSFORMS)
                            childLocalToWorld.unlock();

                        childTransform._hasChanged = false;
                        _flags[childId] |= FLAG_INIT_LOCAL_TO_WORLD;
                        child.localToWorldTransformChanged.execute(child, childLocalToWorld);

                        if (childFlags & FLAG_SYNCHRONIZE_TRANSFORMS)
                        {
                            var childWorldToLocal : Matrix4x4 = _worldToLocalTransforms[childId]
                                || (_worldToLocalTransforms[childId] = new Matrix4x4());

                            if (childFlags & FLAG_LOCK_TRANSFORMS)
                                childWorldToLocal.lock();

                            childWorldToLocal.copyFrom(childLocalToWorld).invert();

                            if (childFlags & FLAG_LOCK_TRANSFORMS)
                                childWorldToLocal.unlock();
                        }
                    }
                }

                if (subtreeOnly && nodeId && nodeId >= subtreeMax)
                {
                    // jump to the first brother who has children
                    var parentId : uint = _parentId[nodeId];

                    nodeId = _firstChildId[parentId];
                    while (!_numChildren[nodeId] && nodeId < subtreeMax)
                        ++nodeId;

                    if (nodeId >= subtreeMax)
                        return ;

                    nodeId = _firstChildId[nodeId];
                }
                else
                    ++nodeId;
            }
        }

        private function updateAncestorsAndSelfLocalToWorld(nodeId : int) : void
        {
            var dirtyRoot : int = -1;

            while (nodeId >= 0)
            {
                if ((_transforms[nodeId] as Matrix4x4)._hasChanged || !_flags[nodeId])
                    dirtyRoot = nodeId;

                nodeId = _parentId[nodeId];
            }

            if (dirtyRoot >= 0)
                updateLocalToWorld(dirtyRoot, true);
        }

        private function targetAddedHandler(ctrl	: TransformController,
                                            target	: ISceneNode) : void
        {
            if (_target)
                throw new Error('The TransformController cannot have more than one target.');

            _target = target;
            _invalidList = true;

            if (target is Scene)
            {
                (target as Scene).renderingBegin.add(renderingBeginHandler);
                return;
            }

            if (target is Group)
            {
                var targetGroup : Group = target as Group;

                targetGroup.descendantAdded.add(descendantAddedHandler);
                targetGroup.descendantRemoved.add(descendantRemovedHandler);
            }

            target.added.add(addedHandler);
        }

        private function targetRemovedHandler(ctrl		: TransformController,
                                              target	: ISceneNode) : void
        {
            target.added.remove(addedHandler);

            if (target is Group)
            {
                var targetGroup : Group = target as Group;

                targetGroup.descendantAdded.remove(descendantAddedHandler);
                targetGroup.descendantRemoved.remove(descendantRemovedHandler);
            }

            _invalidList = false;
            _target = null;

            _nodeToId = null;
            _transforms = null;
            _flags = null;
            _localToWorldTransforms = null;
            _worldToLocalTransforms = null;
            _numChildren = null;
            _idToNode = null;
            _parentId = null;
        }

        private function addedHandler(target : ISceneNode, ancestor : Group) : void
        {
            // the controller will remove itself from the node when it's not its own root anymore
            // but it will watch for the 'removed' signal to add itself back if the node becomes
            // its own root again
            _target.removed.add(removedHandler);
            _target.removeController(this);
        }

        private function removedHandler(target : ISceneNode, ancestor : Group) : void
        {
            if (target.root == target)
            {
                target.removed.remove(removedHandler);
                target.addController(this);
            }
        }

        private function descendantAddedHandler(root		: Group,
                                                descendant	: ISceneNode) : void
        {
            _invalidList = true;
        }

        private function descendantRemovedHandler(root			: Group,
                                                  descendant	: ISceneNode) : void
        {
            _invalidList = true;
        }

        private function getNodeId(node : ISceneNode) : uint
        {
            if (_invalidList || !(node in _nodeToId))
                updateTransformsList();

            return _nodeToId[node];
        }

        private function updateTransformsList() : void
        {
            var root    	                : ISceneNode 			= _target.root;
            var nodes   	                : Vector.<ISceneNode> 	= new <ISceneNode>[root];
            var nodeId  	                : uint 					= 0;
            var oldNodeToId                 : Dictionary            = _nodeToId;
            var oldInitialized              : Vector.<uint>         = _flags;
            var oldLocalToWorldTransforms   : Vector.<Matrix4x4>    = _localToWorldTransforms;
            var oldWorldToLocalTransform    : Vector.<Matrix4x4>    = _worldToLocalTransforms;

            _nodeToId = new Dictionary(true);
            _transforms = new <Matrix4x4>[];
            _flags = new <uint>[];
            _localToWorldTransforms = new <Matrix4x4>[];
            _worldToLocalTransforms = new <Matrix4x4>[];
            _numChildren = new <uint>[];
            _firstChildId = new <uint>[];
            _idToNode = new <ISceneNode>[];
            _parentId = new <int>[-1];

            while (nodes.length)
            {
                var node 	: ISceneNode 	= nodes.shift();
                var group   : Group 		= node as Group;

                _nodeToId[node] = nodeId;
                _idToNode[nodeId] = node;
                _transforms[nodeId] = node.transform;

                if (oldNodeToId && node in oldNodeToId)
                {
                    var oldNodeId   : uint  = oldNodeToId[node];

                    _localToWorldTransforms[nodeId] = oldLocalToWorldTransforms[oldNodeId];
                    _worldToLocalTransforms[nodeId] = oldWorldToLocalTransform[oldNodeId];
                    _flags[nodeId] = oldInitialized[oldNodeId];
                }
                else
                {
                    _localToWorldTransforms[nodeId] = new Matrix4x4().lock();
                    _worldToLocalTransforms[nodeId] = null;
                    _flags[nodeId] = FLAG_NONE;
                }

                if (group)
                {
                    var numChildren 	: uint = group.numChildren;
                    var firstChildId 	: uint = nodeId + nodes.length + 1;

                    _numChildren[nodeId] = numChildren;
                    _firstChildId[nodeId] = firstChildId;
                    for (var childId : uint = 0; childId < numChildren; ++childId)
                    {
                        _parentId[uint(firstChildId + childId)] = nodeId;
                        nodes.push(group.getChildAt(childId));
                    }
                }
                else
                {
                    _numChildren[nodeId] = 0;
                    _firstChildId[nodeId] = 0;
                }

                ++nodeId;
            }

            _worldToLocalTransforms.length = _localToWorldTransforms.length;
            _invalidList = false;
        }

        public function getLocalToWorldTransform(node 			: ISceneNode,
                                                 forceUpdate 	: Boolean = false) : Matrix4x4
        {
            var nodeId : uint = getNodeId(node);

            if (forceUpdate)
                updateAncestorsAndSelfLocalToWorld(nodeId);

            return _localToWorldTransforms[nodeId];
        }

        public function getWorldToLocalTransform(node           : ISceneNode,
                                                 forceUpdate    : Boolean   = false) : Matrix4x4
        {
            var nodeId                  : uint      = getNodeId(node);
            var worldToLocalTransform   : Matrix4x4 = _worldToLocalTransforms[nodeId];

            if (!worldToLocalTransform)
            {
                _worldToLocalTransforms[nodeId] = worldToLocalTransform = new Matrix4x4();
                if (!forceUpdate)
                {
                    worldToLocalTransform.copyFrom(_localToWorldTransforms[nodeId]).invert();
                    _flags[nodeId] |= FLAG_INIT_WORLD_TO_LOCAL;
                }
            }

            if (forceUpdate)
                updateAncestorsAndSelfLocalToWorld(nodeId);

            var flags : uint = _flags[nodeId];

            if (!(flags & FLAG_INIT_WORLD_TO_LOCAL))
            {
                _flags[nodeId] |= FLAG_INIT_WORLD_TO_LOCAL;

                if (flags & FLAG_LOCK_TRANSFORMS)
                    worldToLocalTransform.lock();

                worldToLocalTransform
                    .copyFrom(_localToWorldTransforms[nodeId])
                    .invert();

                if (flags & FLAG_LOCK_TRANSFORMS)
                    worldToLocalTransform.unlock();
            }

            return worldToLocalTransform;
        }

        public function setSharedLocalToWorldTransformReference(node      : ISceneNode,
                                                                matrix    : Matrix4x4) : void
        {
            var nodeId : uint = getNodeId(node);

            if (_flags[nodeId] & FLAG_INIT_LOCAL_TO_WORLD)
                matrix.copyFrom(_localToWorldTransforms[nodeId]);
            _localToWorldTransforms[nodeId] = matrix;
        }

        public function setSharedWorldToLocalTransformReference(node      : ISceneNode,
                                                                matrix    : Matrix4x4) : void
        {
            var nodeId : uint = getNodeId(node);

            if (_flags[nodeId] & FLAG_INIT_WORLD_TO_LOCAL)
                matrix.copyFrom(_worldToLocalTransforms[nodeId]);
            _worldToLocalTransforms[nodeId] = matrix;
        }

        public function synchronizeTransforms(node : ISceneNode, enabled : Boolean) : void
        {
            var nodeId : uint = getNodeId(node);

            _flags[nodeId] = enabled
                ? _flags[nodeId] | FLAG_SYNCHRONIZE_TRANSFORMS
                : _flags[nodeId] & ~FLAG_SYNCHRONIZE_TRANSFORMS;
        }

        public function lockTransformsBeforeUpdate(node : ISceneNode, enabled : Boolean) : void
        {
            var nodeId : uint = getNodeId(node);

            _flags[nodeId] = enabled
                ? _flags[nodeId] | FLAG_LOCK_TRANSFORMS
                : _flags[nodeId] & ~FLAG_LOCK_TRANSFORMS;
        }
    }
}

package aerys.minko.scene.controller
{
    import aerys.minko.ns.minko_math;
    import aerys.minko.render.Viewport;
    import aerys.minko.scene.node.Group;
    import aerys.minko.scene.node.ISceneNode;
    import aerys.minko.scene.node.Scene;
    import aerys.minko.type.math.Matrix4x4;

    import flash.display.BitmapData;
    import flash.utils.Dictionary;

    import mx.olap.aggregators.MaxAggregator;

    use namespace minko_math;

    /**
     * The TransformController handles the batched update of all the local to world matrices
     * of a sub-scene. As such, it will only be active on the root node of a sub-scene and will
     * automatically disable itself on other nodes.
     *
     * @author Jean-Marc Le Roux
     *
     */
    public final class TransformController extends AbstractController
    {

        private static const FLAG_NONE                      : uint  = 0;
        private static const FLAG_INIT_LOCAL_TO_WORLD       : uint  = 1;
        private static const FLAG_INIT_WORLD_TO_LOCAL       : uint  = 2;
        private static const FLAG_SYNCHRONIZE_TRANSFORMS    : uint  = 4;
        private static const FLAG_LOCK_TRANSFORMS           : uint  = 8;

        private var _target					: ISceneNode;

        private var _invalidList			: Boolean;

        private var _nodeToId				: Dictionary;
        private var _idToNode				: Vector.<ISceneNode>
        private var _transforms				: Vector.<Matrix4x4>;
        private var _flags	                : Vector.<uint>;
        private var _localToWorldTransforms : Vector.<Matrix4x4>;
        private var _worldToLocalTransforms : Vector.<Matrix4x4>;
        private var _numChildren			: Vector.<uint>;
        private var _firstChildId			: Vector.<uint>;
        private var _parentId				: Vector.<int>;

        public function TransformController()
        {
            super();

            targetAdded.add(targetAddedHandler);
            targetRemoved.add(targetRemovedHandler);
        }

        private function renderingBeginHandler(scene			: Scene,
                                               viewport		: Viewport,
                                               destination	: BitmapData,
                                               time			: Number) : void
        {
            if (_invalidList)
                updateTransformsList();

            if (_transforms.length)
                updateLocalToWorld();
        }

        private function updateRootLocalToWorld(nodeId : uint = 0) : void
        {
            var rootLocalToWorld	: Matrix4x4		= _localToWorldTransforms[nodeId];
            var rootTransform		: Matrix4x4		= _transforms[nodeId];
            var root				: ISceneNode	= _idToNode[nodeId];
            var rootFlags           : uint          = _flags[nodeId];

            if (rootTransform._hasChanged || !(rootFlags & FLAG_INIT_LOCAL_TO_WORLD))
            {
                if (rootFlags & FLAG_LOCK_TRANSFORMS)
                    rootLocalToWorld.lock();

                rootLocalToWorld.copyFrom(rootTransform);

                if (nodeId != 0)
                    rootLocalToWorld.append(_localToWorldTransforms[_parentId[nodeId]]);

                if (rootFlags & FLAG_LOCK_TRANSFORMS)
                    rootLocalToWorld.unlock();

                rootTransform._hasChanged = false;
                _flags[nodeId] |= FLAG_INIT_LOCAL_TO_WORLD;
                root.localToWorldTransformChanged.execute(root, rootLocalToWorld);

                if (rootFlags & FLAG_SYNCHRONIZE_TRANSFORMS)
                {
                    var rootWorldToLocal : Matrix4x4 = _worldToLocalTransforms[nodeId]
                        || (_worldToLocalTransforms[nodeId] = new Matrix4x4());

                    if (rootFlags & FLAG_LOCK_TRANSFORMS)
                        rootWorldToLocal.lock();

                    rootWorldToLocal.copyFrom(rootLocalToWorld).invert();

                    if (rootFlags & FLAG_LOCK_TRANSFORMS)
                        rootWorldToLocal.unlock();
                }
            }
        }

        private function updateLocalToWorld(nodeId : uint = 0, subtreeOnly : Boolean = false) : void
        {
            var numNodes 			: uint 			= _transforms.length;
            var subtreeMax          : uint          = nodeId;

            updateRootLocalToWorld(nodeId);

            while (nodeId < numNodes)
            {
                var localToWorld 	: Matrix4x4	= _localToWorldTransforms[nodeId];
                var numChildren		: uint		= _numChildren[nodeId];
                var firstChildId	: uint		= _firstChildId[nodeId];
                var lastChildId		: uint		= firstChildId + numChildren;
                var isDirty			: Boolean	= localToWorld._hasChanged;

                localToWorld._hasChanged = false;

                if (lastChildId > subtreeMax)
                    subtreeMax = lastChildId;

                for (var childId : uint = firstChildId; childId < lastChildId; ++childId)
                {
                    var childTransform		: Matrix4x4		= _transforms[childId];
                    var childLocalToWorld	: Matrix4x4		= _localToWorldTransforms[childId];
                    var childFlags          : uint          = _flags[childId];
                    var childIsDirty		: Boolean		= isDirty || childTransform._hasChanged
                        || !(childFlags & FLAG_INIT_LOCAL_TO_WORLD);

                    if (childIsDirty)
                    {
                        var child	: ISceneNode	= _idToNode[childId];

                        if (childFlags & FLAG_LOCK_TRANSFORMS)
                            childLocalToWorld.lock();

                        childLocalToWorld
                            .copyFrom(childTransform)
                            .append(localToWorld);

                        if (childFlags & FLAG_LOCK_TRANSFORMS)
                            childLocalToWorld.unlock();

                        childTransform._hasChanged = false;
                        _flags[childId] |= FLAG_INIT_LOCAL_TO_WORLD;
                        child.localToWorldTransformChanged.execute(child, childLocalToWorld);

                        if (childFlags & FLAG_SYNCHRONIZE_TRANSFORMS)
                        {
                            var childWorldToLocal : Matrix4x4 = _worldToLocalTransforms[childId]
                                || (_worldToLocalTransforms[childId] = new Matrix4x4());

                            if (childFlags & FLAG_LOCK_TRANSFORMS)
                                childWorldToLocal.lock();

                            childWorldToLocal.copyFrom(childLocalToWorld).invert();

                            if (childFlags & FLAG_LOCK_TRANSFORMS)
                                childWorldToLocal.unlock();
                        }
                    }
                }

                if (subtreeOnly && nodeId && nodeId >= subtreeMax)
                {
                    // jump to the first brother who has children
                    var parentId : uint = _parentId[nodeId];

                    nodeId = _firstChildId[parentId];
                    while (!_numChildren[nodeId] && nodeId < subtreeMax)
                        ++nodeId;

                    if (nodeId >= subtreeMax)
                        return ;

                    nodeId = _firstChildId[nodeId];
                }
                else
                    ++nodeId;
            }
        }

        private function updateAncestorsAndSelfLocalToWorld(nodeId : int) : void
        {
            var dirtyRoot : int = -1;

            while (nodeId >= 0)
            {
                if ((_transforms[nodeId] as Matrix4x4)._hasChanged
                    || !(_flags[nodeId] & FLAG_INIT_LOCAL_TO_WORLD))
                    dirtyRoot = nodeId;

                nodeId = _parentId[nodeId];
            }

            if (dirtyRoot >= 0)
                updateLocalToWorld(dirtyRoot, true);
        }

        private function targetAddedHandler(ctrl	: TransformController,
                                            target	: ISceneNode) : void
        {
            if (_target)
                throw new Error('The TransformController cannot have more than one target.');

            _target = target;
            _invalidList = true;

            if (target is Scene)
            {
                (target as Scene).renderingBegin.add(renderingBeginHandler);
                return;
            }

            if (target is Group)
            {
                var targetGroup : Group = target as Group;

                targetGroup.descendantAdded.add(descendantAddedHandler);
                targetGroup.descendantRemoved.add(descendantRemovedHandler);
            }

            target.added.add(addedHandler);
        }

        private function targetRemovedHandler(ctrl		: TransformController,
                                              target	: ISceneNode) : void
        {
            target.added.remove(addedHandler);

            if (target is Group)
            {
                var targetGroup : Group = target as Group;

                targetGroup.descendantAdded.remove(descendantAddedHandler);
                targetGroup.descendantRemoved.remove(descendantRemovedHandler);
            }

            _invalidList = false;
            _target = null;

            _nodeToId = null;
            _transforms = null;
            _flags = null;
            _localToWorldTransforms = null;
            _worldToLocalTransforms = null;
            _numChildren = null;
            _idToNode = null;
            _parentId = null;
        }

        private function addedHandler(target : ISceneNode, ancestor : Group) : void
        {
            // the controller will remove itself from the node when it's not its own root anymore
            // but it will watch for the 'removed' signal to add itself back if the node becomes
            // its own root again
            _target.removed.add(removedHandler);
            _target.removeController(this);
        }

        private function removedHandler(target : ISceneNode, ancestor : Group) : void
        {
            if (target.root == target)
            {
                target.removed.remove(removedHandler);
                target.addController(this);
            }
        }

        private function descendantAddedHandler(root		: Group,
                                                descendant	: ISceneNode) : void
        {
            _invalidList = true;
        }

        private function descendantRemovedHandler(root			: Group,
                                                  descendant	: ISceneNode) : void
        {
            _invalidList = true;
        }

        private function getNodeId(node : ISceneNode) : uint
        {
            if (_invalidList || !(node in _nodeToId))
                updateTransformsList();

            return _nodeToId[node];
        }

        private function updateTransformsList() : void
        {
            var root    	                : ISceneNode 			= _target.root;
            var nodes   	                : Vector.<ISceneNode> 	= new <ISceneNode>[root];
            var nodeId  	                : uint 					= 0;
            var oldNodeToId                 : Dictionary            = _nodeToId;
            var oldInitialized              : Vector.<uint>         = _flags;
            var oldLocalToWorldTransforms   : Vector.<Matrix4x4>    = _localToWorldTransforms;
            var oldWorldToLocalTransform    : Vector.<Matrix4x4>    = _worldToLocalTransforms;

            _nodeToId = new Dictionary(true);
            _transforms = new <Matrix4x4>[];
            _flags = new <uint>[];
            _localToWorldTransforms = new <Matrix4x4>[];
            _worldToLocalTransforms = new <Matrix4x4>[];
            _numChildren = new <uint>[];
            _firstChildId = new <uint>[];
            _idToNode = new <ISceneNode>[];
            _parentId = new <int>[-1];

            while (nodes.length)
            {
                var node 	: ISceneNode 	= nodes.shift();
                var group   : Group 		= node as Group;

                _nodeToId[node] = nodeId;
                _idToNode[nodeId] = node;
                _transforms[nodeId] = node.transform;

                if (oldNodeToId && node in oldNodeToId)
                {
                    var oldNodeId   : uint  = oldNodeToId[node];

                    _localToWorldTransforms[nodeId] = oldLocalToWorldTransforms[oldNodeId];
                    _worldToLocalTransforms[nodeId] = oldWorldToLocalTransform[oldNodeId];
                    _flags[nodeId] = oldInitialized[oldNodeId];
                }
                else
                {
                    _localToWorldTransforms[nodeId] = new Matrix4x4().lock();
                    _worldToLocalTransforms[nodeId] = null;
                    _flags[nodeId] = FLAG_NONE;
                }

                if (group)
                {
                    var numChildren 	: uint = group.numChildren;
                    var firstChildId 	: uint = nodeId + nodes.length + 1;

                    _numChildren[nodeId] = numChildren;
                    _firstChildId[nodeId] = firstChildId;
                    for (var childId : uint = 0; childId < numChildren; ++childId)
                    {
                        _parentId[uint(firstChildId + childId)] = nodeId;
                        nodes.push(group.getChildAt(childId));
                    }
                }
                else
                {
                    _numChildren[nodeId] = 0;
                    _firstChildId[nodeId] = 0;
                }

                ++nodeId;
            }

            _worldToLocalTransforms.length = _localToWorldTransforms.length;
            _invalidList = false;
        }

        public function getLocalToWorldTransform(node 			: ISceneNode,
                                                 forceUpdate 	: Boolean = false) : Matrix4x4
        {
            var nodeId : uint = getNodeId(node);

            if (forceUpdate)
                updateAncestorsAndSelfLocalToWorld(nodeId);

            return _localToWorldTransforms[nodeId];
        }

        public function getWorldToLocalTransform(node           : ISceneNode,
                                                 forceUpdate    : Boolean   = false) : Matrix4x4
        {
            var nodeId                  : uint      = getNodeId(node);
            var worldToLocalTransform   : Matrix4x4 = _worldToLocalTransforms[nodeId];

            if (!worldToLocalTransform)
            {
                _worldToLocalTransforms[nodeId] = worldToLocalTransform = new Matrix4x4();
                if (!forceUpdate)
                {
                    worldToLocalTransform.copyFrom(_localToWorldTransforms[nodeId]).invert();
                    _flags[nodeId] |= FLAG_INIT_WORLD_TO_LOCAL;
                }
            }

            if (forceUpdate)
                updateAncestorsAndSelfLocalToWorld(nodeId);

            var flags : uint = _flags[nodeId];

            if (!(flags & FLAG_INIT_WORLD_TO_LOCAL))
            {
                _flags[nodeId] |= FLAG_INIT_WORLD_TO_LOCAL;

                if (flags & FLAG_LOCK_TRANSFORMS)
                    worldToLocalTransform.lock();

                worldToLocalTransform
                    .copyFrom(_localToWorldTransforms[nodeId])
                    .invert();

                if (flags & FLAG_LOCK_TRANSFORMS)
                    worldToLocalTransform.unlock();
            }

            return worldToLocalTransform;
        }

        public function setSharedLocalToWorldTransformReference(node      : ISceneNode,
                                                                matrix    : Matrix4x4) : void
        {
            var nodeId : uint = getNodeId(node);

            if (_flags[nodeId] & FLAG_INIT_LOCAL_TO_WORLD)
                matrix.copyFrom(_localToWorldTransforms[nodeId]);
            _localToWorldTransforms[nodeId] = matrix;
        }

        public function setSharedWorldToLocalTransformReference(node      : ISceneNode,
                                                                matrix    : Matrix4x4) : void
        {
            var nodeId : uint = getNodeId(node);

            if (_flags[nodeId] & FLAG_INIT_WORLD_TO_LOCAL)
                matrix.copyFrom(_worldToLocalTransforms[nodeId]);
            _worldToLocalTransforms[nodeId] = matrix;
        }

        public function synchronizeTransforms(node : ISceneNode, enabled : Boolean) : void
        {
            var nodeId : uint = getNodeId(node);

            _flags[nodeId] = enabled
                ? _flags[nodeId] | FLAG_SYNCHRONIZE_TRANSFORMS
                : _flags[nodeId] & ~FLAG_SYNCHRONIZE_TRANSFORMS;
        }

        public function lockTransformsBeforeUpdate(node : ISceneNode, enabled : Boolean) : void
        {
            var nodeId : uint = getNodeId(node);

            _flags[nodeId] = enabled
                ? _flags[nodeId] | FLAG_LOCK_TRANSFORMS
                : _flags[nodeId] & ~FLAG_LOCK_TRANSFORMS;
        }
    }
}

fix TransformController.updateAncestorsAndSelfLocalToWorld() to read the flags properly


package aerys.minko.render.geometry
{
	import aerys.minko.render.shader.compiler.CRC32;

	import flash.utils.ByteArray;
	import flash.utils.Dictionary;
	import flash.utils.Endian;

	/**
	 * The GeometrySanitizer static class provides methods to clean up
	 * 3D geometry loaded from 3D asset files and make sure it is compliant
	 * with the limitations of the Stage3D API.
	 *
	 * @author Romain Gilliotte
	 *
	 */
	public final class GeometrySanitizer
	{
//		public static const INDEX_LIMIT		: uint = 524270;
//		public static const VERTEX_LIMIT	: uint = 65535;

		public static const INDEX_LIMIT		: uint = 5242;
		public static const VERTEX_LIMIT	: uint = 6553;

		public static function isValid(indexData		: ByteArray,
									   vertexData		: ByteArray,
									   bytesPerVertex	: uint = 12) : Boolean
		{
			var startPosition	: uint	= indexData.position;
			var numIndices		: uint 	= indexData.bytesAvailable >> 2;
			var numVertices		: uint 	= vertexData.bytesAvailable / bytesPerVertex;

			for (var indexId : uint = 0; indexId < numIndices; ++indexId)
				if (indexData.readUnsignedShort() >= numVertices)
					break;

			indexData.position = startPosition;

			return indexId >= numIndices;
		}

		/**
		 * Split vertex and index buffers too big the be rendered.
		 * The index stream limit is 524270, the vertex stream limit is 65536.
		 *
		 * @param inVertexData
		 * @param inIndexData
		 * @param outVertexDatas
		 * @param outIndexDatas
		 * @param dwordsPerVertex
		 */
		public static function splitBuffers(inVertices		: ByteArray,
											inIndices		: Vector.<uint>,
											outVertices		: Vector.<ByteArray>,
											outIndices		: Vector.<ByteArray>,
											bytesPerVertex	: uint = 12) : void
		{
			var inVerticesStartPosition	: uint	= inVertices.position;
			var numVertices				: uint	= inVertices.bytesAvailable / bytesPerVertex;
			var numIndices				: int	= inIndices.length;

			if (numIndices < INDEX_LIMIT && numVertices < VERTEX_LIMIT)
			{
				var indices : ByteArray = new ByteArray();

				indices.endian = Endian.LITTLE_ENDIAN;
				for (var i : uint = 0; i < numIndices; ++i)
					indices.writeShort(inIndices[i]);
				indices.position = 0;

				outVertices.push(inVertices);
				outIndices.push(indices);

				return;
			}

			var totalUsedIndices : int = 0;

			while (numIndices > 0)
			{
				var indexDataLength		: uint				= numIndices;

				// new buffers
				var partialVertexData	: ByteArray			= new ByteArray();
				var partialIndexData	: ByteArray			= new ByteArray();

				// local variables
				var oldVertexIds		: Vector.<uint>		= new Vector.<uint>(3, true);
				var newVertexIds		: Vector.<uint>		= new Vector.<uint>(3, true);
				var newVertexNeeded		: Vector.<Boolean>	= new Vector.<Boolean>(3, true);

				var usedVerticesDic		: Dictionary		= new Dictionary();	// this dictionary maps old and new indices
				var usedVerticesCount	: uint				= 0;				// this counts elements in the dictionary
				var usedIndicesCount	: uint				= 0;				// how many indices have we used?
				var neededVerticesCount	: uint;

				// iterators & limits
				var localVertexId		: uint;

				partialVertexData.endian = Endian.LITTLE_ENDIAN;
				partialIndexData.endian = Endian.LITTLE_ENDIAN;

				while (usedIndicesCount < indexDataLength)
				{
					// check if next triangle fits into the index buffer
					var remainingIndices	: uint		= INDEX_LIMIT - usedIndicesCount;

					if (remainingIndices < 3)
						break ;

					// check if next triangle fits into the vertex buffer
					var remainingVertices	: uint		= VERTEX_LIMIT - usedVerticesCount;

					neededVerticesCount = 0;
					for (localVertexId = 0; localVertexId < 3; ++localVertexId)
					{
						oldVertexIds[localVertexId]	= inIndices[uint(totalUsedIndices + usedIndicesCount + localVertexId)];

						var tmp : Object = usedVerticesDic[oldVertexIds[localVertexId]];

						newVertexNeeded[localVertexId]	= tmp == null;
						newVertexIds[localVertexId]		= uint(tmp);

						if (newVertexNeeded[localVertexId])
							++neededVerticesCount;
					}

					if (remainingVertices < neededVerticesCount)
						break ;

					// it fills, let insert the triangle
					for (localVertexId = 0; localVertexId < 3; ++localVertexId)
					{
						if (newVertexNeeded[localVertexId])
						{
							// copy current vertex into the new array
							partialVertexData.writeBytes(
								inVertices,
								oldVertexIds[localVertexId] * bytesPerVertex,
								bytesPerVertex
							);

							// updates the id the temporary variable, to allow use filling partial index data later
							newVertexIds[localVertexId] = usedVerticesCount;

							// put its previous id in the dictionary
							usedVerticesDic[oldVertexIds[localVertexId]] = usedVerticesCount++;
						}

						partialIndexData.writeShort(newVertexIds[localVertexId]);
					}

					// ... increment indices counter
					usedIndicesCount += 3;
				}

				totalUsedIndices += usedIndicesCount;
				numIndices -= usedIndicesCount;

				partialVertexData.position = 0;
				outVertices.push(partialVertexData);

				partialIndexData.position = 0;
				outIndices.push(partialIndexData);
			}

			inVertices.position = inVerticesStartPosition;
		}

		/**
		 * Removes duplicated entries in a vertex buffer, and rewrite the index buffer according to it.
		 * The buffers are modified in place.
		 *
		 * This method simplifies implementing parsers for file-formats that store per-vertex
		 * and per-triangle information in different buffers, such as collada or OBJ:
		 * merging them in a naive way and calling this method will do the job.
		 *
		 * @param vertexData
		 * @param indexData
		 * @param dwordsPerVertex
		 */
		public static function removeDuplicatedVertices(vertexData		: ByteArray,
														indexData		: ByteArray,
														bytesPerVertex	: uint = 12) : void
		{
			var vertexDataStartPosition		: uint			= vertexData.position;
			var numVertices					: uint			= vertexData.bytesAvailable / bytesPerVertex;
			var indexDataStartPosition		: uint			= indexData.position;
			var numIndices					: uint			= indexData.bytesAvailable >>> 1;
			var hashToNewVertexId			: Object		= {};
			var oldVertexIdToNewVertexId	: Vector.<uint>	= new Vector.<uint>(numVertices, true);
			var newVertexCount				: uint			= 0;
			var newLimit					: uint			= 0;

			for (var oldVertexId : uint = 0; oldVertexId < numVertices; ++oldVertexId)
			{
				vertexData.position = oldVertexId * bytesPerVertex;

				var numFloats	: uint		= bytesPerVertex >>> 2;
				var vertexHash	: String	= '';

				while (numFloats)
				{
					vertexHash += vertexData.readFloat() + '|';
					--numFloats;
				}

				var index		: Object	= hashToNewVertexId[vertexHash];
				var newVertexId	: uint		= 0;

				if (index === null)
				{
					newVertexId = newVertexCount++;
					hashToNewVertexId[vertexHash] = newVertexId;

					newLimit = (1 + newVertexId) * bytesPerVertex;

					if (newVertexId != oldVertexId)
					{
						vertexData.position = newVertexId * bytesPerVertex;
						vertexData.writeBytes(vertexData, oldVertexId * bytesPerVertex, bytesPerVertex);
					}
				}
				else
					newVertexId = uint(index);

				oldVertexIdToNewVertexId[oldVertexId] = newVertexId;
			}

			vertexData.position = vertexDataStartPosition;
			vertexData.length = newLimit;

			for (var indexId : int = 0; indexId < numIndices; ++indexId)
			{
				index = indexData.readUnsignedShort();
				indexData.position -= 2;
				indexData.writeShort(oldVertexIdToNewVertexId[index]);
			}
			indexData.position = indexDataStartPosition;
		}

		/**
		 * Removes unused entries in a vertex buffer, and rewrite the index buffer according to it.
		 * The buffers are modified in place.
		 *
		 * @param vertexData
		 * @param indexData
		 * @param dwordsPerVertex
		 */
		public static function removeUnusedVertices(vertexData		: ByteArray,
													indexData		: ByteArray,
													bytesPerVertex	: uint = 12) : void
		{
			var vertexDataStartPosition	: uint	= vertexData.position;
			var oldNumVertices			: uint	= vertexData.bytesAvailable / bytesPerVertex;
			var newNumVertices			: uint	= 0;
			var indexDataStartPosition	: uint	= indexData.position;
			var numIndices				: uint	= indexData.bytesAvailable >>> 1;

			// iterators
			var oldVertexId		: uint;
			var indexId			: uint;

			// flag unused vertices by scanning the index buffer
			var oldVertexIdToUsage : Vector.<Boolean> = new Vector.<Boolean>(oldNumVertices, true);
			for (indexId = 0; indexId < numIndices; ++indexId)
				oldVertexIdToUsage[indexData.readUnsignedShort()] = true;

			// scan the flags, fix vertex buffer, and store old to new vertex id mapping
			var oldVertexIdToNewVertexId : Vector.<uint> = new Vector.<uint>(oldNumVertices, true);
			for (oldVertexId = 0; oldVertexId < oldNumVertices; ++oldVertexId)
				if (oldVertexIdToUsage[oldVertexId])
				{
					var newVertexId	: uint = newNumVertices++;

					// store new index
					oldVertexIdToNewVertexId[oldVertexId] = newVertexId;

					// rewrite vertexbuffer in place
					if (newVertexId != oldVertexId)
					{
						vertexData.position = newVertexId * bytesPerVertex;
						vertexData.writeBytes(vertexData, oldVertexId * bytesPerVertex, bytesPerVertex);
					}
				}

			vertexData.position = vertexDataStartPosition;
			vertexData.length = newNumVertices * bytesPerVertex;

			// rewrite index buffer in place
			indexData.position = indexDataStartPosition;
			for (indexId = 0; indexId < numIndices; ++indexId)
			{
				var index : uint = indexData.readUnsignedShort();

				indexData.position -= 2;
				indexData.writeShort(oldVertexIdToNewVertexId[index]);
			}
			indexData.position = indexDataStartPosition;
		}
	}
}

package aerys.minko.render.geometry
{
	import aerys.minko.render.shader.compiler.CRC32;

	import flash.utils.ByteArray;
	import flash.utils.Dictionary;
	import flash.utils.Endian;

	/**
	 * The GeometrySanitizer static class provides methods to clean up
	 * 3D geometry loaded from 3D asset files and make sure it is compliant
	 * with the limitations of the Stage3D API.
	 *
	 * @author Romain Gilliotte
	 *
	 */
	public final class GeometrySanitizer
	{
		public static const INDEX_LIMIT		: uint = 524270;
		public static const VERTEX_LIMIT	: uint = 65535;

		public static function isValid(indexData		: ByteArray,
									   vertexData		: ByteArray,
									   bytesPerVertex	: uint = 12) : Boolean
		{
			var startPosition	: uint	= indexData.position;
			var numIndices		: uint 	= indexData.bytesAvailable >> 2;
			var numVertices		: uint 	= vertexData.bytesAvailable / bytesPerVertex;

			for (var indexId : uint = 0; indexId < numIndices; ++indexId)
				if (indexData.readUnsignedShort() >= numVertices)
					break;

			indexData.position = startPosition;

			return indexId >= numIndices;
		}

		/**
		 * Split vertex and index buffers too big the be rendered.
		 * The index stream limit is 524270, the vertex stream limit is 65536.
		 *
		 * @param inVertexData
		 * @param inIndexData
		 * @param outVertexDatas
		 * @param outIndexDatas
		 * @param dwordsPerVertex
		 */
		public static function splitBuffers(inVertices		: ByteArray,
											inIndices		: Vector.<uint>,
											outVertices		: Vector.<ByteArray>,
											outIndices		: Vector.<ByteArray>,
											bytesPerVertex	: uint = 12) : void
		{
			var inVerticesStartPosition	: uint	= inVertices.position;
			var numVertices				: uint	= inVertices.bytesAvailable / bytesPerVertex;
			var numIndices				: int	= inIndices.length;

			if (numIndices < INDEX_LIMIT && numVertices < VERTEX_LIMIT)
			{
				var indices : ByteArray = new ByteArray();

				indices.endian = Endian.LITTLE_ENDIAN;
				for (var i : uint = 0; i < numIndices; ++i)
					indices.writeShort(inIndices[i]);
				indices.position = 0;

				outVertices.push(inVertices);
				outIndices.push(indices);

				return;
			}

			var totalUsedIndices : int = 0;

			while (numIndices > 0)
			{
				var indexDataLength		: uint				= numIndices;

				// new buffers
				var partialVertexData	: ByteArray			= new ByteArray();
				var partialIndexData	: ByteArray			= new ByteArray();

				// local variables
				var oldVertexIds		: Vector.<uint>		= new Vector.<uint>(3, true);
				var newVertexIds		: Vector.<uint>		= new Vector.<uint>(3, true);
				var newVertexNeeded		: Vector.<Boolean>	= new Vector.<Boolean>(3, true);

				var usedVerticesDic		: Dictionary		= new Dictionary();	// this dictionary maps old and new indices
				var usedVerticesCount	: uint				= 0;				// this counts elements in the dictionary
				var usedIndicesCount	: uint				= 0;				// how many indices have we used?
				var neededVerticesCount	: uint;

				// iterators & limits
				var localVertexId		: uint;

				partialVertexData.endian = Endian.LITTLE_ENDIAN;
				partialIndexData.endian = Endian.LITTLE_ENDIAN;

				while (usedIndicesCount < indexDataLength)
				{
					// check if next triangle fits into the index buffer
					var remainingIndices	: uint		= INDEX_LIMIT - usedIndicesCount;

					if (remainingIndices < 3)
						break ;

					// check if next triangle fits into the vertex buffer
					var remainingVertices	: uint		= VERTEX_LIMIT - usedVerticesCount;

					neededVerticesCount = 0;
					for (localVertexId = 0; localVertexId < 3; ++localVertexId)
					{
						oldVertexIds[localVertexId]	= inIndices[uint(totalUsedIndices + usedIndicesCount + localVertexId)];

						var tmp : Object = usedVerticesDic[oldVertexIds[localVertexId]];

						newVertexNeeded[localVertexId]	= tmp == null;
						newVertexIds[localVertexId]		= uint(tmp);

						if (newVertexNeeded[localVertexId])
							++neededVerticesCount;
					}

					if (remainingVertices < neededVerticesCount)
						break ;

					// it fills, let insert the triangle
					for (localVertexId = 0; localVertexId < 3; ++localVertexId)
					{
						if (newVertexNeeded[localVertexId])
						{
							// copy current vertex into the new array
							partialVertexData.writeBytes(
								inVertices,
								oldVertexIds[localVertexId] * bytesPerVertex,
								bytesPerVertex
							);

							// updates the id the temporary variable, to allow use filling partial index data later
							newVertexIds[localVertexId] = usedVerticesCount;

							// put its previous id in the dictionary
							usedVerticesDic[oldVertexIds[localVertexId]] = usedVerticesCount++;
						}

						partialIndexData.writeShort(newVertexIds[localVertexId]);
					}

					// ... increment indices counter
					usedIndicesCount += 3;
				}

				totalUsedIndices += usedIndicesCount;
				numIndices -= usedIndicesCount;

				partialVertexData.position = 0;
				outVertices.push(partialVertexData);

				partialIndexData.position = 0;
				outIndices.push(partialIndexData);
			}

			inVertices.position = inVerticesStartPosition;
		}

		/**
		 * Removes duplicated entries in a vertex buffer, and rewrite the index buffer according to it.
		 * The buffers are modified in place.
		 *
		 * This method simplifies implementing parsers for file-formats that store per-vertex
		 * and per-triangle information in different buffers, such as collada or OBJ:
		 * merging them in a naive way and calling this method will do the job.
		 *
		 * @param vertexData
		 * @param indexData
		 * @param dwordsPerVertex
		 */
		public static function removeDuplicatedVertices(vertexData		: ByteArray,
														indexData		: ByteArray,
														bytesPerVertex	: uint = 12) : void
		{
			var vertexDataStartPosition		: uint			= vertexData.position;
			var numVertices					: uint			= vertexData.bytesAvailable / bytesPerVertex;
			var indexDataStartPosition		: uint			= indexData.position;
			var numIndices					: uint			= indexData.bytesAvailable >>> 1;
			var hashToNewVertexId			: Object		= {};
			var oldVertexIdToNewVertexId	: Vector.<uint>	= new Vector.<uint>(numVertices, true);
			var newVertexCount				: uint			= 0;
			var newLimit					: uint			= 0;

			for (var oldVertexId : uint = 0; oldVertexId < numVertices; ++oldVertexId)
			{
				vertexData.position = oldVertexId * bytesPerVertex;

				var numFloats	: uint		= bytesPerVertex >>> 2;
				var vertexHash	: String	= '';

				while (numFloats)
				{
					vertexHash += vertexData.readFloat() + '|';
					--numFloats;
				}

				var index		: Object	= hashToNewVertexId[vertexHash];
				var newVertexId	: uint		= 0;

				if (index === null)
				{
					newVertexId = newVertexCount++;
					hashToNewVertexId[vertexHash] = newVertexId;

					newLimit = (1 + newVertexId) * bytesPerVertex;

					if (newVertexId != oldVertexId)
					{
						vertexData.position = newVertexId * bytesPerVertex;
						vertexData.writeBytes(vertexData, oldVertexId * bytesPerVertex, bytesPerVertex);
					}
				}
				else
					newVertexId = uint(index);

				oldVertexIdToNewVertexId[oldVertexId] = newVertexId;
			}

			vertexData.position = vertexDataStartPosition;
			vertexData.length = newLimit;

			for (var indexId : int = 0; indexId < numIndices; ++indexId)
			{
				index = indexData.readUnsignedShort();
				indexData.position -= 2;
				indexData.writeShort(oldVertexIdToNewVertexId[index]);
			}
			indexData.position = indexDataStartPosition;
		}

		/**
		 * Removes unused entries in a vertex buffer, and rewrite the index buffer according to it.
		 * The buffers are modified in place.
		 *
		 * @param vertexData
		 * @param indexData
		 * @param dwordsPerVertex
		 */
		public static function removeUnusedVertices(vertexData		: ByteArray,
													indexData		: ByteArray,
													bytesPerVertex	: uint = 12) : void
		{
			var vertexDataStartPosition	: uint	= vertexData.position;
			var oldNumVertices			: uint	= vertexData.bytesAvailable / bytesPerVertex;
			var newNumVertices			: uint	= 0;
			var indexDataStartPosition	: uint	= indexData.position;
			var numIndices				: uint	= indexData.bytesAvailable >>> 1;

			// iterators
			var oldVertexId		: uint;
			var indexId			: uint;

			// flag unused vertices by scanning the index buffer
			var oldVertexIdToUsage : Vector.<Boolean> = new Vector.<Boolean>(oldNumVertices, true);
			for (indexId = 0; indexId < numIndices; ++indexId)
				oldVertexIdToUsage[indexData.readUnsignedShort()] = true;

			// scan the flags, fix vertex buffer, and store old to new vertex id mapping
			var oldVertexIdToNewVertexId : Vector.<uint> = new Vector.<uint>(oldNumVertices, true);
			for (oldVertexId = 0; oldVertexId < oldNumVertices; ++oldVertexId)
				if (oldVertexIdToUsage[oldVertexId])
				{
					var newVertexId	: uint = newNumVertices++;

					// store new index
					oldVertexIdToNewVertexId[oldVertexId] = newVertexId;

					// rewrite vertexbuffer in place
					if (newVertexId != oldVertexId)
					{
						vertexData.position = newVertexId * bytesPerVertex;
						vertexData.writeBytes(vertexData, oldVertexId * bytesPerVertex, bytesPerVertex);
					}
				}

			vertexData.position = vertexDataStartPosition;
			vertexData.length = newNumVertices * bytesPerVertex;

			// rewrite index buffer in place
			indexData.position = indexDataStartPosition;
			for (indexId = 0; indexId < numIndices; ++indexId)
			{
				var index : uint = indexData.readUnsignedShort();

				indexData.position -= 2;
				indexData.writeShort(oldVertexIdToNewVertexId[index]);
			}
			indexData.position = indexDataStartPosition;
		}
	}
}

fix wrong values for GeometrySanitizer.INDEX_LIMIT and GeometrySanitizer.VERTEX_LIMIT


//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2007 Three Rings Design, Inc., All Rights Reserved
// http://www.threerings.net/code/narya/
//
// This library is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA

package com.threerings.io {

import flash.utils.ByteArray;

import com.threerings.util.ClassUtil;
import com.threerings.util.Cloneable;

public dynamic class TypedArray extends Array
    implements Cloneable
{
    /**
     * Convenience method to get the java type of an array containing objects of the specified
     * class.
     */
    public static function getJavaType (of :Class) :String
    {
        if (of === Boolean) {
            return "[Z";

        } else if (of === int) { // Number will be int if something like 3.0
            return "[I";

        } else if (of === Number) {
            return "[D";

        } else if (of === ByteArray) {
            return "[[B";
        }

        var cname :String = Translations.getToServer(ClassUtil.getClassName(of));
        return "[L" + cname + ";";
    }

    /**
     * A factory method to create a TypedArray for holding objects of the specified type.
     */
    public static function create (of :Class) :TypedArray
    {
        return new TypedArray(getJavaType(of));
    }

    /**
     * Create a TypedArray
     *
     * @param jtype The java classname of this array, for example "[I" to represent an int[], or
     * "[Ljava.lang.Object;" for Object[].
     */
    public function TypedArray (jtype :String)
    {
        _jtype = jtype;
    }

    /**
     * Adds all of the elements of the supplied array to this typed array. The types of the
     * elements of the target array must, of course, be of the type specified for this array
     * otherwise badness will ensue.
     *
     * @return this array instance for handy call chainability.
     */
    public function addAll (other :Array) :TypedArray
    {
        push.apply(this, other);
        return this;
    }

    public function getJavaType () :String
    {
        return _jtype;
    }

    // from Cloneable
    public function clone () :Object
    {
        var clazz :Class = ClassUtil.getClass(this);
        var copy :TypedArray = new clazz(_jtype);
        copy.addAll(this);
        return copy;
    }

    /** The 'type' of this array, which doesn't really mean anything except gives it a clue as to
     * how to stream to our server. */
    protected var _jtype :String;
}
}

//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2007 Three Rings Design, Inc., All Rights Reserved
// http://www.threerings.net/code/narya/
//
// This library is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA

package com.threerings.io {

import flash.utils.ByteArray;

import com.threerings.util.ClassUtil;
import com.threerings.util.Cloneable;

public dynamic class TypedArray extends Array
    implements Cloneable
{
    /**
     * Convenience method to get the java type of an array containing objects of the specified
     * class.
     */
    public static function getJavaType (of :Class) :String
    {
        if (of === Boolean) {
            return "[Z";

        } else if (of === int) { // Number will be int if something like 3.0
            return "[I";

        } else if (of === Number) {
            return "[D";

        } else if (of === ByteArray) {
            return "[[B";
        }

        var cname :String = Translations.getToServer(ClassUtil.getClassName(of));
        return "[L" + cname + ";";
    }

    /**
     * A factory method to create a TypedArray for holding objects of the specified type.
     */
    public static function create (of :Class, initialValues :Array = null) :TypedArray
    {
        var ta :TypedArray = new TypedArray(getJavaType(of));
        if (initialValues != null) {
            ta.addAll(initialValues);
        }
        return ta;
    }

    /**
     * Create a TypedArray
     *
     * @param jtype The java classname of this array, for example "[I" to represent an int[], or
     * "[Ljava.lang.Object;" for Object[].
     */
    public function TypedArray (jtype :String)
    {
        _jtype = jtype;
    }

    /**
     * Adds all of the elements of the supplied array to this typed array. The types of the
     * elements of the target array must, of course, be of the type specified for this array
     * otherwise badness will ensue.
     *
     * @return this array instance for handy call chainability.
     */
    public function addAll (other :Array) :TypedArray
    {
        push.apply(this, other);
        return this;
    }

    public function getJavaType () :String
    {
        return _jtype;
    }

    // from Cloneable
    public function clone () :Object
    {
        var clazz :Class = ClassUtil.getClass(this);
        var copy :TypedArray = new clazz(_jtype);
        copy.addAll(this);
        return copy;
    }

    /** The 'type' of this array, which doesn't really mean anything except gives it a clue as to
     * how to stream to our server. */
    protected var _jtype :String;
}
}

Add an easy way to initialize.
Jeez, this whole thing could *almost* go away if we moved to flash 10
and used Vector, but of course, there's no way to get the type of
Vector. Maybe there is, with describeType(). Can't experiment now.
I'd fall out of my chair if there were.
We need to know in case sending an empty array to the server,
it's still gotta be the right type (with non-empty we could
try examining the first element).

git-svn-id: a1a4b28b82a3276cc491891159dd9963a0a72fae@5742 542714f4-19e9-0310-aa3c-eee0fc999fb1


//
// Flump - Copyright 2013 Flump Authors

package flump.display {

import flash.geom.Matrix;
import flash.geom.Point;
import flash.geom.Rectangle;

import flump.mold.LayerMold;
import flump.mold.MovieMold;

import react.Signal;

import starling.animation.IAnimatable;
import starling.display.DisplayObject;
import starling.display.Sprite;
import starling.events.Event;
import starling.utils.MatrixUtil;

/**
 * A movie created from flump-exported data. It has children corresponding to the layers in the
 * movie in Flash, in the same order and with the same names. It fills in those children
 * initially with the image or movie of the symbol on that exported layer. After the initial
 * population, it only applies the keyframe-based transformations to the child at the index
 * corresponding to the layer. This means it's safe to swap in other DisplayObjects at those
 * positions to have them animated in place of the initial child.
 *
 *
A Movie will not animate unless it's added to a Juggler (or its advanceTime() function
 * is otherwise called). When the movie is added to a juggler, it advances its playhead with the
 * frame ticks if isPlaying is true. It will automatically remove itself from its juggler when
 * removed from the stage.

 *
 * @see Library and LibraryLoader to create instances of Movie.
 */
public class Movie extends Sprite
    implements IAnimatable
{
    /** A label fired by all movies when entering their first frame. */
    public static const FIRST_FRAME :String = "flump.movie.FIRST_FRAME";

    /** A label fired by all movies when entering their last frame. */
    public static const LAST_FRAME :String = "flump.movie.LAST_FRAME";

    /** Fires the label string whenever it's passed in playing. */
    public const labelPassed :Signal = new Signal(String);

    /** @private */
    public function Movie (src :MovieMold, frameRate :Number, library :Library) {
        this.name = src.id;
        _labels = src.labels;
        _frameRate = frameRate;
        if (src.flipbook) {
            _layers = new Vector.<Layer>(1, true);
            _layers[0] = createLayer(this, src.layers[0], library, /*flipbook=*/true);
            _numFrames = src.layers[0].frames;
        } else {
            _layers = new Vector.<Layer>(src.layers.length, true);
            for (var ii :int = 0; ii < _layers.length; ii++) {
                _layers[ii] = createLayer(this, src.layers[ii], library, /*flipbook=*/false);
                _numFrames = Math.max(src.layers[ii].frames, _numFrames);
            }
        }
        _duration = _numFrames / _frameRate;
        updateFrame(0, 0);

        // When we're removed from the stage, remove ourselves from any juggler animating us.
        addEventListener(Event.REMOVED_FROM_STAGE, function (..._) :void {
            dispatchEventWith(Event.REMOVE_FROM_JUGGLER);
        });
    }

    /** @return the frame being displayed. */
    public function get frame () :int { return _frame; }

    /** @return the number of frames in the movie. */
    public function get numFrames () :int { return _numFrames; }

    /** @return true if the movie is currently playing. */
    public function get isPlaying () :Boolean { return _state == PLAYING; }

    /** @return true if the movie contains the given label. */
    public function hasLabel (label :String) :Boolean {
        return getFrameForLabel(label) >= 0;
    }

    /** @return the frame index for the given label, or -1 if the label doesn't exist. */
    public function getFrameForLabel (label :String) :int {
        for (var ii :int = 0; ii < _labels.length; ii++) {
            if (_labels[ii] != null && _labels[ii].indexOf(label) != -1) return ii;
        }
        return -1;
    }

    /** Plays the movie from its current frame. The movie will loop forever.  */
    public function loop () :Movie {
        _state = PLAYING;
        _stopFrame = NO_FRAME;
        return this;
    }

    /** Plays the movie from its current frame, stopping when it reaches its last frame. */
    public function playOnce () :Movie { return playTo(LAST_FRAME); }

    /**
     * Moves to the given String label or int frame. Doesn't alter playing status or stop frame.
     * If there are labels at the given position, they're fired as part of the goto, even if the
     * current frame is equal to the destination. Labels between the current frame and the
     * destination frame are not fired.
     *
     * @param position the int frame or String label to goto.
     *
     * @return this movie for chaining
     *
     * @throws Error if position isn't an int or String, or if it is a String and that String isn't
     * a label on this movie
     */
    public function goTo (position :Object) :Movie {
        const frame :int = extractFrame(position);
        updateFrame(frame, 0);
        return this;
    }

   /**
    * Plays the movie from its current frame. The movie will stop when it reaches the given label
    * or frame.
    *
    * @param position to int frame or String label to stop at
    *
    * @return this movie for chaining
    *
    * @throws Error if position isn't an int or String, or if it is a String and that String isn't
    * a label on this movie.
    */
    public function playTo (position :Object) :Movie {
       // won't play if we're already at the stop position
       return stopAt(position).play();
    }

    /**
     * Sets the stop frame for this Movie.
     *
     * @param position the int frame or String label to stop at.
     *
     * @return this movie for chaining
     *
     * @throws Error if position isn't an int or String, or if it is a String and that String isn't
     * a label on this movie.
     */
    public function stopAt (position :Object) :Movie {
        _stopFrame = extractFrame(position);
        return this;
    }

    /**
     * Sets the movie playing. It will automatically stop at its stopFrame, if one is set,
     * otherwise it will loop forever.
     *
     * @return this movie for chaining
     */
    public function play () :Movie {
        // set playing to true unless movie is at the stop frame
        _state = (_frame != _stopFrame ? PLAYING : STOPPED);
        return this;
    }

    /** Stops playback if it's currently active. Doesn't alter the current frame or stop frame. */
    public function stop () :Movie {
        _state = STOPPED;
        return this;
    }

    /** Stops playback of this movie, but not its children */
    public function playChildrenOnly () :Movie {
        _state = PLAYING_CHILDREN_ONLY;
        return this;
    }

    /** Advances the playhead by the give number of seconds. From IAnimatable. */
    public function advanceTime (dt :Number) :void {
        if (dt < 0) {
            throw new Error("Invalid time [dt=" + dt + "]");
        }

        if (_skipAdvanceTime) {
            _skipAdvanceTime = false;
            return;
        }

        if (_state == STOPPED) {
            return;
        }

        if (_state == PLAYING && _numFrames > 1) {
            _playTime += dt;
            var actualPlaytime :Number = _playTime;
            if (_playTime >= _duration) {
                _playTime %= _duration;
            }

            // If _playTime is very close to _duration, rounding error can cause us to
            // land on lastFrame + 1. Protect against that.
            var newFrame :int = int(_playTime * _frameRate);
            if (newFrame < 0) {
                newFrame = 0;
            } else if (newFrame >= _numFrames) {
                newFrame = _numFrames - 1;
            }

            // If the update crosses or goes to the stopFrame:
            // go to the stopFrame, stop the movie, clear the stopFrame
            if (_stopFrame != NO_FRAME) {
                // how many frames remain to the stopframe?
                var framesRemaining :int =
                    (_frame <= _stopFrame ? _stopFrame - _frame : _numFrames - _frame + _stopFrame);
                var framesElapsed :int = int(actualPlaytime * _frameRate) - _frame;
                if (framesElapsed >= framesRemaining) {
                    _state = STOPPED;
                    newFrame = _stopFrame;
                }
            }
            updateFrame(newFrame, dt);
        }

        for each (var layer :Layer in _layers) {
            layer.advanceTime(dt);
        }
    }

    /**
     * @public
     *
     * Modified from starling.display.DisplayObjectContainer
     */
    public override function getBounds (targetSpace :DisplayObject, resultRect :Rectangle=null) :Rectangle {
        if (resultRect == null) {
            resultRect = new Rectangle();
        } else {
            resultRect.setEmpty();
        }

        // get bounds from layer contents
        for each (var layer :Layer in _layers) {
            layer.expandBounds(targetSpace, resultRect);
        }

        // if no contents exist, simply include this movie's position in the bounds
        if (resultRect.isEmpty()) {
            getTransformationMatrix(targetSpace, IDENTITY_MATRIX);
            MatrixUtil.transformCoords(IDENTITY_MATRIX, 0.0, 0.0, HELPER_POINT);
            resultRect.setTo(HELPER_POINT.x, HELPER_POINT.y, 0, 0);
        }

        return resultRect;
    }

    /**
     * @private
     *
     * Called when the Movie has been newly added to a layer.
     */
    internal function addedToLayer () :void {
        goTo(0);
        _skipAdvanceTime = true;
    }

    /** @private */
    protected function extractFrame (position :Object) :int {
        if (position is int) return int(position);
        if (!(position is String)) throw new Error("Movie position must be an int frame or String label");
        const label :String = String(position);
        var frame :int = getFrameForLabel(label);
        if (frame < 0) {
            throw new Error("No such label '" + label + "'");
        }
        return frame;
    }

    /**
     * @private
     *
     * @param dt the timeline's elapsed time since the last update. This should be 0
     * for updates that are the result of a "goTo" call.
     */
    protected function updateFrame (newFrame :int, dt :Number) :void {
        if (newFrame < 0 || newFrame >= _numFrames) {
            throw new Error("Invalid frame [frame=" + newFrame + ", validRange=0-" + (_numFrames - 1) + "]");
        }

        if (_isUpdatingFrame) {
            _pendingFrame = newFrame;
            return;
        }

        _pendingFrame = NO_FRAME;
        _isUpdatingFrame = true;

        const isGoTo :Boolean = (dt <= 0);
        const wrapped :Boolean = (dt >= _duration) || (newFrame < _frame);

        if (newFrame != _frame) {
            for each (var layer :Layer in _layers) {
                layer.drawFrame(newFrame);
            }
        }

        if (isGoTo) {
            _playTime = newFrame / _frameRate;
        }

        // Update the frame before firing frame label signals, so if firing changes the frame,
        // it sticks.
        const oldFrame :int = _frame;
        _frame = newFrame;

        // determine which labels to fire signals for
        var startFrame :int;
        var frameCount :int;
        if (isGoTo) {
            startFrame = newFrame;
            frameCount = 1;
        } else {
            startFrame = (oldFrame + 1 < _numFrames ? oldFrame + 1 : 0);
            frameCount = (_frame - oldFrame);
            if (wrapped) {
                frameCount += _numFrames;
            }
        }

        // Fire signals. Stop if pendingFrame is updated, which indicates that the client
        // has called goTo()
        var frameIdx :int = startFrame;
        for (var ii :int = 0; ii < frameCount; ++ii) {
            if (_pendingFrame != NO_FRAME) break;

            if (_labels[frameIdx] != null) {
                for each (var label :String in _labels[frameIdx]) {
                    labelPassed.emit(label);
                    if (_pendingFrame != NO_FRAME) break;
                }
            }

            // avoid modulo division by updating frameIdx each time through the loop
            if (++frameIdx == _numFrames) {
                frameIdx = 0;
            }
        }

        _isUpdatingFrame = false;
        // If we were interrupted by a goTo(), update to that frame now.
        if (_pendingFrame != NO_FRAME) {
            newFrame = _pendingFrame;
            updateFrame(newFrame, 0);
        }
    }

    protected function createLayer (movie :Movie, src :LayerMold, library :Library, flipbook :Boolean) :Layer {
        return new Layer(movie, src, library, flipbook);
    }

    /** @private */
    protected var _isUpdatingFrame :Boolean;
    /** @private */
    protected var _pendingFrame :int = NO_FRAME;
    /** @private */
    protected var _frame :int = NO_FRAME, _stopFrame :int = NO_FRAME;
    /** @private */
    protected var _state :int = PLAYING;
    /** @private */
    protected var _playTime :Number = 0;
    /** @private */
    protected var _duration :Number;
    /** @private */
    protected var _layers :Vector.<Layer>;
    /** @private */
    protected var _numFrames :int;
    /** @private */
    protected var _frameRate :Number;
    /** @private */
    protected var _labels :Vector.<Vector.<String>>;
    /** @private */
    private var _skipAdvanceTime :Boolean = false;
    /** @private */
    internal var _playerData :MoviePlayerNode;

    private static const HELPER_POINT :Point = new Point();
    private static const IDENTITY_MATRIX :Matrix = new Matrix();

    private static const NO_FRAME :int = -1;

    private static const STOPPED :int = 0;
    private static const PLAYING_CHILDREN_ONLY :int = 1;
    private static const PLAYING :int = 2;
}
}

//
// Flump - Copyright 2013 Flump Authors

package flump.display {

import flash.geom.Matrix;
import flash.geom.Point;
import flash.geom.Rectangle;

import flump.mold.LayerMold;
import flump.mold.MovieMold;

import react.Signal;

import starling.animation.IAnimatable;
import starling.display.DisplayObject;
import starling.display.Sprite;
import starling.events.Event;
import starling.utils.MatrixUtil;

/**
 * A movie created from flump-exported data. It has children corresponding to the layers in the
 * movie in Flash, in the same order and with the same names. It fills in those children
 * initially with the image or movie of the symbol on that exported layer. After the initial
 * population, it only applies the keyframe-based transformations to the child at the index
 * corresponding to the layer. This means it's safe to swap in other DisplayObjects at those
 * positions to have them animated in place of the initial child.
 *
 *
A Movie will not animate unless it's added to a Juggler (or its advanceTime() function
 * is otherwise called). When the movie is added to a juggler, it advances its playhead with the
 * frame ticks if isPlaying is true. It will automatically remove itself from its juggler when
 * removed from the stage.

 *
 * @see Library and LibraryLoader to create instances of Movie.
 */
public class Movie extends Sprite
    implements IAnimatable
{
    /** A label fired by all movies when entering their first frame. */
    public static const FIRST_FRAME :String = "flump.movie.FIRST_FRAME";

    /** A label fired by all movies when entering their last frame. */
    public static const LAST_FRAME :String = "flump.movie.LAST_FRAME";

    /** Fires the label string whenever it's passed in playing. */
    public const labelPassed :Signal = new Signal(String);

    /** @private */
    public function Movie (src :MovieMold, frameRate :Number, library :Library) {
        this.name = src.id;
        _labels = src.labels;
        _frameRate = frameRate;
        if (src.flipbook) {
            _layers = new Vector.<Layer>(1, true);
            _layers[0] = createLayer(this, src.layers[0], library, /*flipbook=*/true);
            _numFrames = src.layers[0].frames;
        } else {
            _layers = new Vector.<Layer>(src.layers.length, true);
            for (var ii :int = 0; ii < _layers.length; ii++) {
                _layers[ii] = createLayer(this, src.layers[ii], library, /*flipbook=*/false);
                _numFrames = Math.max(src.layers[ii].frames, _numFrames);
            }
        }
        _duration = _numFrames / _frameRate;
        updateFrame(0, 0);

        // When we're removed from the stage, remove ourselves from any juggler animating us.
        addEventListener(Event.REMOVED_FROM_STAGE, function (..._) :void {
            dispatchEventWith(Event.REMOVE_FROM_JUGGLER);
        });
    }

    /** @return the frame being displayed. */
    public function get frame () :int { return _frame; }

    /** @return the number of frames in the movie. */
    public function get numFrames () :int { return _numFrames; }

    /** @return true if the movie is currently playing. */
    public function get isPlaying () :Boolean { return _state == PLAYING; }

    /** @return true if the movie contains the given label. */
    public function hasLabel (label :String) :Boolean {
        return getFrameForLabel(label) >= 0;
    }

    /** @return the frame index for the given label, or -1 if the label doesn't exist. */
    public function getFrameForLabel (label :String) :int {
        for (var ii :int = 0; ii < _labels.length; ii++) {
            if (_labels[ii] != null && _labels[ii].indexOf(label) != -1) return ii;
        }
        return -1;
    }

    /** Plays the movie from its current frame. The movie will loop forever.  */
    public function loop () :Movie {
        _state = PLAYING;
        _stopFrame = NO_FRAME;
        return this;
    }

    /** Plays the movie from its current frame, stopping when it reaches its last frame. */
    public function playOnce () :Movie { return playTo(LAST_FRAME); }

    /**
     * Moves to the given String label or int frame. Doesn't alter playing status or stop frame.
     * If there are labels at the given position, they're fired as part of the goto, even if the
     * current frame is equal to the destination. Labels between the current frame and the
     * destination frame are not fired.
     *
     * @param position the int frame or String label to goto.
     *
     * @return this movie for chaining
     *
     * @throws Error if position isn't an int or String, or if it is a String and that String isn't
     * a label on this movie
     */
    public function goTo (position :Object) :Movie {
        const frame :int = extractFrame(position);
        updateFrame(frame, 0);
        return this;
    }

   /**
    * Plays the movie from its current frame. The movie will stop when it reaches the given label
    * or frame.
    *
    * @param position to int frame or String label to stop at
    *
    * @return this movie for chaining
    *
    * @throws Error if position isn't an int or String, or if it is a String and that String isn't
    * a label on this movie.
    */
    public function playTo (position :Object) :Movie {
       // won't play if we're already at the stop position
       return stopAt(position).play();
    }

    /**
     * Sets the stop frame for this Movie.
     *
     * @param position the int frame or String label to stop at.
     *
     * @return this movie for chaining
     *
     * @throws Error if position isn't an int or String, or if it is a String and that String isn't
     * a label on this movie.
     */
    public function stopAt (position :Object) :Movie {
        _stopFrame = extractFrame(position);
        return this;
    }

    /**
     * Sets the movie playing. It will automatically stop at its stopFrame, if one is set,
     * otherwise it will loop forever.
     *
     * @return this movie for chaining
     */
    public function play () :Movie {
        // set playing to true unless movie is at the stop frame
        _state = (_frame != _stopFrame ? PLAYING : STOPPED);
        return this;
    }

    /** Stops playback if it's currently active. Doesn't alter the current frame or stop frame. */
    public function stop () :Movie {
        _state = STOPPED;
        return this;
    }

    /** Stops playback of this movie, but not its children */
    public function playChildrenOnly () :Movie {
        _state = PLAYING_CHILDREN_ONLY;
        return this;
    }

    /** Advances the playhead by the give number of seconds. From IAnimatable. */
    public function advanceTime (dt :Number) :void {
        if (dt < 0) {
            throw new Error("Invalid time [dt=" + dt + "]");
        }

        if (_skipAdvanceTime) {
            _skipAdvanceTime = false;
            return;
        }

        if (_state == STOPPED) {
            return;
        }

        if (_state == PLAYING && _numFrames > 1) {
            _playTime += dt;
            var actualPlaytime :Number = _playTime;
            if (_playTime >= _duration) {
                _playTime %= _duration;
            }

            // If _playTime is very close to _duration, rounding error can cause us to
            // land on lastFrame + 1. Protect against that.
            var newFrame :int = int(_playTime * _frameRate);
            if (newFrame < 0) {
                newFrame = 0;
            } else if (newFrame >= _numFrames) {
                newFrame = _numFrames - 1;
            }

            // If the update crosses or goes to the stopFrame:
            // go to the stopFrame, stop the movie, clear the stopFrame
            if (_stopFrame != NO_FRAME) {
                // how many frames remain to the stopframe?
                var framesRemaining :int =
                    (_frame <= _stopFrame ? _stopFrame - _frame : _numFrames - _frame + _stopFrame);
                var framesElapsed :int = int(actualPlaytime * _frameRate) - _frame;
                if (framesElapsed >= framesRemaining) {
                    _state = STOPPED;
                    newFrame = _stopFrame;
                }
            }
            updateFrame(newFrame, dt);
        }

        for each (var layer :Layer in _layers) {
            layer.advanceTime(dt);
        }
    }

    /**
     * @public
     *
     * Modified from starling.display.DisplayObjectContainer
     */
    public override function getBounds (targetSpace :DisplayObject, resultRect :Rectangle=null) :Rectangle {
        if (resultRect == null) {
            resultRect = new Rectangle();
        } else {
            resultRect.setEmpty();
        }

        // get bounds from layer contents
        for each (var layer :Layer in _layers) {
            layer.expandBounds(targetSpace, resultRect);
        }

        // if no contents exist, simply include this movie's position in the bounds
        if (resultRect.isEmpty()) {
            getTransformationMatrix(targetSpace, IDENTITY_MATRIX);
            MatrixUtil.transformCoords(IDENTITY_MATRIX, 0.0, 0.0, HELPER_POINT);
            resultRect.setTo(HELPER_POINT.x, HELPER_POINT.y, 0, 0);
        }

        return resultRect;
    }

    /**
     * @private
     *
     * Called when the Movie has been newly added to a layer.
     */
    internal function addedToLayer () :void {
        goTo(0);
        _skipAdvanceTime = true;
    }

    /** @private */
    protected function extractFrame (position :Object) :int {
        if (position is int) return int(position);
        if (!(position is String)) throw new Error("Movie position must be an int frame or String label");
        const label :String = String(position);
        var frame :int = getFrameForLabel(label);
        if (frame < 0) {
            throw new Error("No such label '" + label + "'");
        }
        return frame;
    }

    /**
     * @private
     *
     * @param dt the timeline's elapsed time since the last update. This should be 0
     * for updates that are the result of a "goTo" call.
     */
    protected function updateFrame (newFrame :int, dt :Number) :void {
        if (newFrame < 0 || newFrame >= _numFrames) {
            throw new Error("Invalid frame [frame=" + newFrame + ", validRange=0-" + (_numFrames - 1) + "]");
        }

        if (_isUpdatingFrame) {
            _pendingFrame = newFrame;
            return;
        }

        _pendingFrame = NO_FRAME;
        _isUpdatingFrame = true;

        const isGoTo :Boolean = (dt <= 0);
        const wrapped :Boolean = (dt >= _duration) || (newFrame < _frame);

        if (newFrame != _frame) {
            for each (var layer :Layer in _layers) {
                layer.drawFrame(newFrame);
            }
        }

        if (isGoTo) {
            _playTime = newFrame / _frameRate;
        }

        // Update the frame before firing frame label signals, so if firing changes the frame,
        // it sticks.
        const oldFrame :int = _frame;
        _frame = newFrame;

        // determine which labels to fire signals for
        var startFrame :int;
        var frameCount :int;
        if (isGoTo) {
            startFrame = newFrame;
            frameCount = 1;
        } else {
            startFrame = (oldFrame + 1 < _numFrames ? oldFrame + 1 : 0);
            frameCount = (_frame - oldFrame);
            if (wrapped) {
                frameCount += _numFrames;
            }
        }

        // Fire signals. Stop if pendingFrame is updated, which indicates that the client
        // has called goTo()
        var frameIdx :int = startFrame;
        for (var ii :int = 0; ii < frameCount; ++ii) {
            if (_pendingFrame != NO_FRAME) break;

            if (_labels[frameIdx] != null) {
                for each (var label :String in _labels[frameIdx]) {
                    labelPassed.emit(label);
                    if (_pendingFrame != NO_FRAME) break;
                }
            }

            // avoid modulo division by updating frameIdx each time through the loop
            if (++frameIdx == _numFrames) {
                frameIdx = 0;
            }
        }

        _isUpdatingFrame = false;
        // If we were interrupted by a goTo(), update to that frame now.
        if (_pendingFrame != NO_FRAME) {
            newFrame = _pendingFrame;
            updateFrame(newFrame, 0);
        }
    }

    protected function createLayer (movie :Movie, src :LayerMold, library :Library, flipbook :Boolean) :Layer {
        return new Layer(movie, src, library, flipbook);
    }

    /** @private */
    protected var _isUpdatingFrame :Boolean;
    /** @private */
    protected var _pendingFrame :int = NO_FRAME;
    /** @private */
    protected var _frame :int = NO_FRAME, _stopFrame :int = NO_FRAME;
    /** @private */
    protected var _state :String = PLAYING;
    /** @private */
    protected var _playTime :Number = 0;
    /** @private */
    protected var _duration :Number;
    /** @private */
    protected var _layers :Vector.<Layer>;
    /** @private */
    protected var _numFrames :int;
    /** @private */
    protected var _frameRate :Number;
    /** @private */
    protected var _labels :Vector.<Vector.<String>>;
    /** @private */
    private var _skipAdvanceTime :Boolean = false;
    /** @private */
    internal var _playerData :MoviePlayerNode;

    private static const HELPER_POINT :Point = new Point();
    private static const IDENTITY_MATRIX :Matrix = new Matrix();

    private static const NO_FRAME :int = -1;

    private static const STOPPED :String = "STOPPED";
    private static const PLAYING_CHILDREN_ONLY :String = "PLAYING_CHILDREN_ONLY";
    private static const PLAYING :String = "PLAYING";
}
}

use Strings for states

use Strings for states


package
{
    import flash.errors.IllegalOperationError;
    import flash.events.Event;
    import flash.events.EventDispatcher;
    import flash.events.IOErrorEvent;
    import flash.events.ProgressEvent;
    import flash.events.SecurityErrorEvent;
    import flash.events.TextEvent;
    import flash.net.Socket;
    import flash.utils.ByteArray;
    import flash.utils.clearTimeout;
    import flash.utils.setTimeout;

    import swfcat;

    public class ProxyPair extends EventDispatcher
    {
        private var ui:swfcat;

        protected var client_addr:Object;

        /* Not defined here: subclasses should define their own
         * protected var client_socket:Object;
         */

        private var c2r_schedule:Array;

        private var relay_addr:Object;
        private var relay_socket:Socket;
        private var r2c_schedule:Array;

        // Callback id.
        private var flush_id:uint;

        public function ProxyPair(self:ProxyPair, ui:swfcat)
        {
            if (self != this) {
                //only a subclass can pass a valid reference to self
            	throw new IllegalOperationError("ProxyPair cannot be instantiated directly.");
            }

            this.ui = ui;
            this.c2r_schedule = new Array();
            this.r2c_schedule = new Array();

            setup_relay_socket();

            /* client_socket setup should be taken */
            /* care of in the subclass constructor */
        }

        public function close():void
        {
            if (relay_socket != null && relay_socket.connected) {
                relay_socket.close();
            }

            /* subclasses should override to close */
            /* their client_socket according to impl. */
        }

        public function get connected():Boolean
        {
            return (relay_socket != null && relay_socket.connected);

            /* subclasses should override to check */
            /* connectivity of their client_socket. */
        }

        public function set client(client_addr:Object):void
        {
            /* subclasses should override to */
            /* connect the client_socket here */
        }

        public function set relay(relay_addr:Object):void
        {
            this.relay_addr = relay_addr;
            log("Relay: connecting to " + relay_addr.host + ":" + relay_addr.port + ".");
            relay_socket.connect(relay_addr.host, relay_addr.port);
        }

        protected function transfer_bytes(src:Object, dst:Object, num_bytes:uint):void
        {
            /* No-op: must be overridden by subclasses */
        }

        protected function socket_error(message:String):Function
        {
            return function(e:Event):void {
                if (e is TextEvent)
                    log(message + ": " + (e as TextEvent).text + ".");
                else
                    log(message + ".");
                close();
            };
        }

        private function setup_relay_socket():void
        {
            relay_socket = new Socket();
            relay_socket.addEventListener(Event.CONNECT, function (e:Event):void {
                log("Relay: connected to " + relay_addr.host + ":" + relay_addr.port + ".");
                if (connected) {
                    dispatchEvent(new Event(Event.CONNECT));
                }
            });
            relay_socket.addEventListener(Event.CLOSE, socket_error("Relay: closed"));
            relay_socket.addEventListener(IOErrorEvent.IO_ERROR, socket_error("Relay: I/O error"))
            relay_socket.addEventListener(SecurityErrorEvent.SECURITY_ERROR, socket_error("Relay: security error"))
            relay_socket.addEventListener(ProgressEvent.SOCKET_DATA, relay_to_client);
        }

        protected function client_to_relay(e:ProgressEvent):void
        {
            c2r_schedule.push(e.bytesLoaded);
            flush();
        }

        private function relay_to_client(e:ProgressEvent):void
        {
            r2c_schedule.push(e.bytesLoaded);
            flush();
        }

        /* Send as much data as the rate limit currently allows. */
        private function flush():void
        {
            if (flush_id)
                clearTimeout(flush_id);
            flush_id = undefined;

            if (!connected)
                /* Can't do anything until connected. */
                return;

            while (!ui.rate_limit.is_limited() && (c2r_schedule.length > 0 || r2c_schedule.length > 0)) {
                var num_bytes:uint;

                if (c2r_schedule.length > 0) {
                    num_bytes = c2r_schedule.shift();
                    transfer_bytes(null, relay_socket, num_bytes);
                    ui.rate_limit.update(num_bytes);
                }

                if (r2c_schedule.length > 0) {
                    num_bytes = r2c_schedule.shift();
                    transfer_bytes(relay_socket, null, num_bytes);
                    ui.rate_limit.update(num_bytes);
                }
            }

            /* Call again when safe, if necessary. */
            if (c2r_schedule.length > 0 || r2c_schedule.length > 0)
                flush_id = setTimeout(flush, ui.rate_limit.when() * 1000);
        }

        /* Helper function to write output to the
         * swfcat console. Set as protected for
         * subclasses */
        protected function log(s:String):void
        {
            ui.puts(s);
        }
    }
}

package
{
    import flash.events.Event;
    import flash.events.EventDispatcher;
    import flash.events.IOErrorEvent;
    import flash.events.ProgressEvent;
    import flash.events.SecurityErrorEvent;
    import flash.events.TextEvent;
    import flash.net.Socket;
    import flash.utils.ByteArray;
    import flash.utils.clearTimeout;
    import flash.utils.setTimeout;

    /* An instance of a client-relay connection. */
    public class ProxyPair extends EventDispatcher
    {
        // Socket to client.
        private var s_c:*;
        private var connect_c:Function;

        // Socket to relay.
        private var s_r:*;
        private var connect_r:Function;

        // Parent swfcat, for UI updates and rate meter.
        private var ui:swfcat;

        // Pending byte read counts for relay and client sockets.
        private var r2c_schedule:Array;
        private var c2r_schedule:Array;
        // Callback id.
        private var flush_id:uint;

        public function log(msg:String):void
        {
            ui.puts(id() + ": " + msg)
        }

        // String describing this pair for output.
        public function id():String
        {
            return "<>";
        }

        public function ProxyPair(ui:swfcat, s_c:*, connect_c:Function, s_r:*, connect_r:Function)
        {
            this.ui = ui;
            /* s_c is a socket for connecting to the client. connect_c is a
               function that, when called, connects s_c. Likewise for s_r and
               connect_r. */
            this.s_c = s_c;
            this.connect_c = connect_c;
            this.s_r = s_r;
            this.connect_r = connect_r;

            this.c2r_schedule = [];
            this.r2c_schedule = [];
        }

        /* Return a function that shows an error message and closes the other half
           of a communication pair. */
        private function socket_error(message:String, other:*):Function
        {
            return function(e:Event):void {
                if (e is TextEvent)
                    log(message + ": " + (e as TextEvent).text + ".");
                else
                    log(message + ".");
                if (other && other.connected)
                    other.close();
                dispatchEvent(new Event(Event.COMPLETE));
            };
        }

        public function connect():void
        {
            s_r.addEventListener(Event.CONNECT, relay_connected);
            s_r.addEventListener(Event.CLOSE, socket_error("Relay: closed", s_c));
            s_r.addEventListener(IOErrorEvent.IO_ERROR, socket_error("Relay: I/O error", s_c));
            s_r.addEventListener(SecurityErrorEvent.SECURITY_ERROR, socket_error("Relay: security error", s_c));
            s_r.addEventListener(ProgressEvent.SOCKET_DATA, relay_to_client);

            s_c.addEventListener(Event.CONNECT, client_connected);
            s_c.addEventListener(Event.CLOSE, socket_error("Client: closed", s_r));
            s_c.addEventListener(IOErrorEvent.IO_ERROR, socket_error("Client: I/O error", s_r));
            s_c.addEventListener(SecurityErrorEvent.SECURITY_ERROR, socket_error("Client: security error", s_r));
            s_c.addEventListener(ProgressEvent.SOCKET_DATA, client_to_relay);

            log("Relay: connecting.");
            connect_r();
            log("Client: connecting.");
            connect_c();
        }

        private function relay_connected(e:Event):void
        {
            log("Relay: connected.");
        }

        private function client_connected(e:Event):void
        {
            log("Client: connected.");
        }

        private function relay_to_client(e:ProgressEvent):void
        {
            r2c_schedule.push(e.bytesLoaded);
            flush();
        }

        private function client_to_relay(e:ProgressEvent):void
        {
            c2r_schedule.push(e.bytesLoaded);
            flush();
        }

        private function transfer_chunk(s_from:*, s_to:*, n:uint,
            label:String):void
        {
            var bytes:ByteArray;

            bytes = new ByteArray();
            s_from.readBytes(bytes, 0, n);
            s_to.writeBytes(bytes);
            ui.rate_limit.update(n);
            log(label + ": read " + bytes.length + ".");
        }

        /* Send as much data as the rate limit currently allows. */
        private function flush():void
        {
            if (flush_id)
                clearTimeout(flush_id);
            flush_id = undefined;

            if (!(s_r.connected && s_c.connected))
                /* Can't do anything until both sockets are connected. */
                return;

            while (!ui.rate_limit.is_limited() &&
                   (r2c_schedule.length > 0 || c2r_schedule.length > 0)) {
                if (r2c_schedule.length > 0)
                    transfer_chunk(s_r, s_c, r2c_schedule.shift(), "Relay");
                if (c2r_schedule.length > 0)
                    transfer_chunk(s_c, s_r, c2r_schedule.shift(), "Client");
            }

            /* Call again when safe, if necessary. */
            if (r2c_schedule.length > 0 || c2r_schedule.length > 0)
                flush_id = setTimeout(flush, ui.rate_limit.when() * 1000);
        }
    }
}

Rewrite ProxyPair to be more generic.

Rewrite ProxyPair to be more generic.