package {

    import flash.display.Sprite;
    import flash.events.Event;
    import flash.events.DataEvent;
    import flash.events.IOErrorEvent;
    import flash.events.ProgressEvent;
    import flash.events.SecurityErrorEvent;
    import flash.events.TimerEvent;
    import flash.net.Socket;
    import flash.system.System;
    import flash.utils.getTimer;
    import flash.utils.Timer;
    import flash.utils.getQualifiedClassName;
    import flash.sampler.*;

    public class Agent extends Sprite {
	    private static const HOST:String = "localhost";
	    private static const PORT:int = 42624;
	    private static const PREFIX:String = "[AGENT]";

	    private var _host:String;
	    private var _port:int;
	    private var _socket:Socket;
	    private var _connected:Boolean;
	    private var _sampleSender:Timer;

    	public function Agent() {
    	    trace(PREFIX, "Loaded");

    	    _host = loaderInfo.parameters["host"] || HOST;
    	    _port = loaderInfo.parameters["port"] || PORT;

	        _socket = new Socket();

            _socket.addEventListener(SecurityErrorEvent.SECURITY_ERROR, fail);
            _socket.addEventListener(IOErrorEvent.IO_ERROR, fail);
            _socket.addEventListener(ProgressEvent.SOCKET_DATA, dataReceived);
            _socket.addEventListener(Event.CONNECT, connected);
            _socket.addEventListener(Event.CLOSE, close);

            connect();
    	}

    	private function dataReceived(e:ProgressEvent):void {
    	    if( _socket.bytesAvailable < 2 ) {
    	        // All commands are two-bytes.
    	        return;
    	    }

    	    var command:uint = _socket.readUnsignedShort();

    	    trace(PREFIX, "Received command", command);

    	    switch( command ) {
                case 0x4202:
                    _socket.writeShort(0x4203);
                    _socket.writeUnsignedInt(getTimer());
                    _socket.writeUnsignedInt(System.totalMemory);
                    _socket.flush();
                    return;

//                case "START SAMPLING":
//                    startSampling();
//                    _socket.send("OK START " + new Date().time );
//                    return;
//
//                case "PAUSE SAMPLING":
//                    pauseSampling();
//                    _socket.send("OK PAUSE");
//                    return;
//
//                case "STOP SAMPLING":
//                    stopSampling();
//                    _socket.send("OK STOP");
//                    return;
//
//                case "GET SAMPLES":
//                    var count:int = getSampleCount();
//
//                    trace(PREFIX, "Sending", count, "samples");
//
//                    _socket.send("SENDING SAMPLES: " + count);
//
//                    var samples:Array = []
//
//                    for each (var s:Sample in getSamples()) {
//                        samples.push(s);
//                    }
//
//                    var batchSize:int = 1000;
//                    var offset:int = 0;
//
//                    _sampleSender = new Timer(100, Math.ceil(count / batchSize) );
//                    _sampleSender.addEventListener(TimerEvent.TIMER, function(e:Event):void {
//                        var toSend:int = Math.min(count, offset + batchSize);
//
//                        trace(PREFIX, "Sending", offset, "-", toSend);
//
//                        for( var i:int = offset; i < toSend; i++ ) {
//                            _socket.send(sampleToString(samples[i]));
//
//                            if( i % 100 == 0) {
//                                trace(PREFIX, "Sent", i, "/", toSend);
//                            }
//                        }
//
//                        offset += batchSize;
//                    });
//                    _sampleSender.addEventListener(TimerEvent.TIMER_COMPLETE, function(e:Event):void {
//                        trace(PREFIX, "Done sending samples");
//
//                        _sampleSender = null;
//                    });
//                    _sampleSender.start();
//
//                    return;
//
//                case "CLEAR SAMPLES":
//                    clearSamples();
//
//                    _socket.send("OK CLEARED");
//                    return;

                default:
                    _socket.writeShort(0x4200);
                    _socket.writeUTF("UNKNOWN COMMAND");
                    _socket.flush();
                    return;
    	    }
    	}

    	private function sampleToString(s:Sample):String {
            if( s is NewObjectSample ) {
                var nos:NewObjectSample = s as NewObjectSample;

                return "[NewObjectSample" +
                    "\n   time: " + nos.time +
                    "\n     id: " + nos.id +
                    "\n   type: " + getQualifiedClassName(nos.type) +
                    stackToString(nos.stack) +
                    "\n]";

            } else if( s is DeleteObjectSample ) {
                var dos:DeleteObjectSample = s as DeleteObjectSample;

                return "[DeleteObjectSample" +
                    "\n   time: " + dos.time +
                    "\n     id: " + dos.id +
                    "\n   size: " + dos.size +
                    stackToString(dos.stack) +
                    "\n]";
            }

            return "[Sample" +
                "\n   time: " + s.time +
                stackToString(s.stack) +
                "\n]";
    	}

    	private function stackToString(stack:Array):String {
    	    if( null == stack ) {
    	        return "";
    	    }

    	    var separator:String = "\n    ";
    	    return "\n  stack: " + separator + stack.join(separator);
    	}

    	private function connect():void {
    	    trace(PREFIX, "Trying to connect to", _host, ":", _port);

    	    try {
    	        _socket.connect(_host, _port);
	        } catch (e:Error) {
	            trace(PREFIX, "Unable to connect", e);
	        }
    	}

    	private function connected(e:Event):void {
    	   _connected = true;

    	   trace(PREFIX, "Connected");

           var now:uint = new Date().time;
           var seconds:uint = now / 1000;
           var timer:uint = getTimer();

           _socket.writeShort(0x4201);
           _socket.writeUnsignedInt(seconds);
           _socket.writeShort(now - (seconds * 1000));
           _socket.writeUnsignedInt(timer);
           _socket.flush();

           trace(PREFIX, seconds, now - (seconds * 1000), timer);
    	}

    	private function close(e:Event):void {
    	   _connected = false;

    	   trace(PREFIX, "Disconnected");
    	}

    	private function fail(e:Event):void {
            trace(PREFIX, "Communication failure", e);

    	    _socket.close();
    	    _connected = false;
    	}
    }
}

package {

    import flash.display.Sprite;
    import flash.events.Event;
    import flash.events.DataEvent;
    import flash.events.IOErrorEvent;
    import flash.events.ProgressEvent;
    import flash.events.SecurityErrorEvent;
    import flash.events.TimerEvent;
    import flash.net.Socket;
    import flash.system.System;
    import flash.utils.getTimer;
    import flash.utils.Timer;
    import flash.utils.getQualifiedClassName;
    import flash.sampler.*;

    public class Agent extends Sprite {
	    private static const HOST:String = "localhost";
	    private static const PORT:int = 42624;
	    private static const PREFIX:String = "[AGENT]";

	    private var _host:String;
	    private var _port:int;
	    private var _socket:Socket;
	    private var _connected:Boolean;
	    private var _sampleSender:Timer;

    	public function Agent() {
    	    trace(PREFIX, "Loaded");

    	    _host = loaderInfo.parameters["host"] || HOST;
    	    _port = loaderInfo.parameters["port"] || PORT;

	        _socket = new Socket();

            _socket.addEventListener(SecurityErrorEvent.SECURITY_ERROR, fail);
            _socket.addEventListener(IOErrorEvent.IO_ERROR, fail);
            _socket.addEventListener(ProgressEvent.SOCKET_DATA, dataReceived);
            _socket.addEventListener(Event.CONNECT, connected);
            _socket.addEventListener(Event.CLOSE, close);

            connect();
    	}

    	private function dataReceived(e:ProgressEvent):void {
    	    if( _socket.bytesAvailable < 2 ) {
    	        // All commands are two-bytes.
    	        return;
    	    }

    	    var command:uint = _socket.readUnsignedShort();

    	    trace(PREFIX, "Received command", command);

    	    switch( command ) {
                case 0x4202:
                    _socket.writeShort(0x4203);
                    _socket.writeUnsignedInt(getTimer());
                    _socket.writeUnsignedInt(System.totalMemory);
                    _socket.flush();
                    return;

//                case "START SAMPLING":
//                    startSampling();
//                    _socket.send("OK START " + new Date().time );
//                    return;
//
//                case "PAUSE SAMPLING":
//                    pauseSampling();
//                    _socket.send("OK PAUSE");
//                    return;
//
//                case "STOP SAMPLING":
//                    stopSampling();
//                    _socket.send("OK STOP");
//                    return;
//
//                case "GET SAMPLES":
//                    var count:int = getSampleCount();
//
//                    trace(PREFIX, "Sending", count, "samples");
//
//                    _socket.send("SENDING SAMPLES: " + count);
//
//                    var samples:Array = []
//
//                    for each (var s:Sample in getSamples()) {
//                        samples.push(s);
//                    }
//
//                    var batchSize:int = 1000;
//                    var offset:int = 0;
//
//                    _sampleSender = new Timer(100, Math.ceil(count / batchSize) );
//                    _sampleSender.addEventListener(TimerEvent.TIMER, function(e:Event):void {
//                        var toSend:int = Math.min(count, offset + batchSize);
//
//                        trace(PREFIX, "Sending", offset, "-", toSend);
//
//                        for( var i:int = offset; i < toSend; i++ ) {
//                            _socket.send(sampleToString(samples[i]));
//
//                            if( i % 100 == 0) {
//                                trace(PREFIX, "Sent", i, "/", toSend);
//                            }
//                        }
//
//                        offset += batchSize;
//                    });
//                    _sampleSender.addEventListener(TimerEvent.TIMER_COMPLETE, function(e:Event):void {
//                        trace(PREFIX, "Done sending samples");
//
//                        _sampleSender = null;
//                    });
//                    _sampleSender.start();
//
//                    return;
//
//                case "CLEAR SAMPLES":
//                    clearSamples();
//
//                    _socket.send("OK CLEARED");
//                    return;

                default:
                    _socket.writeShort(0x4200);
                    _socket.writeUTF("UNKNOWN COMMAND");
                    _socket.flush();
                    return;
    	    }
    	}

    	private function sampleToString(s:Sample):String {
            if( s is NewObjectSample ) {
                var nos:NewObjectSample = s as NewObjectSample;

                return "[NewObjectSample" +
                    "\n   time: " + nos.time +
                    "\n     id: " + nos.id +
                    "\n   type: " + getQualifiedClassName(nos.type) +
                    stackToString(nos.stack) +
                    "\n]";

            } else if( s is DeleteObjectSample ) {
                var dos:DeleteObjectSample = s as DeleteObjectSample;

                return "[DeleteObjectSample" +
                    "\n   time: " + dos.time +
                    "\n     id: " + dos.id +
                    "\n   size: " + dos.size +
                    stackToString(dos.stack) +
                    "\n]";
            }

            return "[Sample" +
                "\n   time: " + s.time +
                stackToString(s.stack) +
                "\n]";
    	}

    	private function stackToString(stack:Array):String {
    	    if( null == stack ) {
    	        return "";
    	    }

    	    var separator:String = "\n    ";
    	    return "\n  stack: " + separator + stack.join(separator);
    	}

    	private function connect():void {
    	    trace(PREFIX, "Trying to connect to", _host, ":", _port);

    	    try {
    	        _socket.connect(_host, _port);
	        } catch (e:Error) {
	            trace(PREFIX, "Unable to connect", e);
	        }
    	}

    	private function connected(e:Event):void {
    	   _connected = true;

    	   trace(PREFIX, "Connected");

           var now:Number = new Date().getTime();
           var seconds:uint = now / 1000;
           var timer:uint = getTimer();

           _socket.writeShort(0x4201);
           _socket.writeUnsignedInt(seconds);
           _socket.writeShort(now - (seconds * 1000));
           _socket.writeUnsignedInt(timer);
           _socket.flush();

           trace(PREFIX, now, seconds, now - (seconds * 1000), timer);
    	}

    	private function close(e:Event):void {
    	   _connected = false;

    	   trace(PREFIX, "Disconnected");
    	}

    	private function fail(e:Event):void {
            trace(PREFIX, "Communication failure", e);

    	    _socket.close();
    	    _connected = false;
    	}
    }
}

use getTime() since its accurate.

use getTime() since its accurate.


/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mangui.hls.loader {
    import org.mangui.hls.model.Level;

    import flash.utils.getTimer;
    import flash.utils.clearTimeout;
    import flash.utils.setTimeout;
    import flash.events.ErrorEvent;
    import flash.events.SecurityErrorEvent;
    import flash.events.IOErrorEvent;

    import org.mangui.hls.HLSSettings;
    import org.mangui.hls.constant.HLSPlayStates;
    import org.mangui.hls.model.AudioTrack;
    import org.mangui.hls.model.Fragment;
    import org.mangui.hls.event.HLSError;
    import org.mangui.hls.event.HLSEvent;
    import org.mangui.hls.playlist.AltAudioTrack;
    import org.mangui.hls.playlist.Manifest;

    import flash.net.URLLoader;

    import org.mangui.hls.HLS;

    public class AltAudioLevelLoader {
        CONFIG::LOGGING {
            import org.mangui.hls.utils.Log;
        }
        /** Reference to the hls framework controller. **/
        private var _hls : HLS;
        /** Object that fetches the manifest. **/
        private var _urlloader : URLLoader;
        /** Link to the M3U8 file. **/
        private var _url : String;
        /** Timeout ID for reloading live playlists. **/
        private var _timeoutID : uint;
        /** last reload manifest time **/
        private var _reload_playlists_timer : uint;
        /** current audio level **/
        private var _current_track : int;
        /** reference to manifest being loaded **/
        private var _manifest_loading : Manifest;
        /** is this loader closed **/
        private var _closed : Boolean = false;
        /* playlist retry timeout */
        private var _retry_timeout : Number;
        private var _retry_count : int;

        /** Setup the loader. **/
        public function AltAudioLevelLoader(hls : HLS) {
            _hls = hls;
            _hls.addEventListener(HLSEvent.PLAYBACK_STATE, _stateHandler);
            _hls.addEventListener(HLSEvent.AUDIO_TRACK_SWITCH, _audioTrackSwitchHandler);
        };

        public function dispose() : void {
            _close();
            _hls.removeEventListener(HLSEvent.PLAYBACK_STATE, _stateHandler);
            _hls.removeEventListener(HLSEvent.AUDIO_TRACK_SWITCH, _audioTrackSwitchHandler);
        }

        /** Loading failed; return errors. **/
        private function _errorHandler(event : ErrorEvent) : void {
            var txt : String;
            var code : int;
            if (event is SecurityErrorEvent) {
                code = HLSError.MANIFEST_LOADING_CROSSDOMAIN_ERROR;
                txt = "Cannot load M3U8: crossdomain access denied:" + event.text;
            } else if (event is IOErrorEvent && (HLSSettings.manifestLoadMaxRetry == -1 || _retry_count < HLSSettings.manifestLoadMaxRetry)) {
                CONFIG::LOGGING {
                    Log.warn("I/O Error while trying to load Playlist, retry in " + _retry_timeout + " ms");
                }
                _timeoutID = setTimeout(_loadAudioLevelPlaylist, _retry_timeout);
                /* exponential increase of retry timeout, capped to manifestLoadMaxRetryTimeout */
                _retry_timeout = Math.min(HLSSettings.manifestLoadMaxRetryTimeout, 2 * _retry_timeout);
                _retry_count++;
                return;
            } else {
                code = HLSError.MANIFEST_LOADING_IO_ERROR;
                txt = "Cannot load M3U8: " + event.text;
            }
            var hlsError : HLSError = new HLSError(code, _url, txt);
            _hls.dispatchEvent(new HLSEvent(HLSEvent.ERROR, hlsError));
        };

        /** parse a playlist **/
        private function _parseAudioPlaylist(string : String, url : String, level : int) : void {
            if (string != null && string.length != 0) {
                CONFIG::LOGGING {
                    Log.debug("audio level " + level + " playlist:\n" + string);
                }
                var frags : Vector.<Fragment> = Manifest.getFragments(string, url, level);
                // set fragment and update sequence number range
                var newLevel : Level = new Level();
                newLevel.updateFragments(frags);
                newLevel.targetduration = Manifest.getTargetDuration(string);
                _hls.audioTracks[_current_track].level = newLevel;
            }
            _hls.dispatchEvent(new HLSEvent(HLSEvent.AUDIO_LEVEL_LOADED, level));
            _manifest_loading = null;
        };

        /** load/reload active M3U8 playlist **/
        private function _loadAudioLevelPlaylist() : void {
            if (_closed) {
                return;
            }
            _reload_playlists_timer = getTimer();
            var altAudioTrack : AltAudioTrack = _hls.altAudioTracks[_hls.audioTracks[_current_track].id];
            _manifest_loading = new Manifest();
            _manifest_loading.loadPlaylist(altAudioTrack.url, _parseAudioPlaylist, _errorHandler, _current_track, _hls.type, HLSSettings.flushLiveURLCache);
            _hls.dispatchEvent(new HLSEvent(HLSEvent.AUDIO_LEVEL_LOADING, _current_track));
        };

        /** When audio track switch occurs, assess the need of loading audio level playlist **/
        private function _audioTrackSwitchHandler(event : HLSEvent) : void {
            _current_track = event.audioTrack;
            var audioTrack : AudioTrack = _hls.audioTracks[_current_track];
            if (audioTrack.source == AudioTrack.FROM_PLAYLIST) {
                var altAudioTrack : AltAudioTrack = _hls.altAudioTracks[audioTrack.id];
                if (altAudioTrack.url && audioTrack.level == null) {
                    CONFIG::LOGGING {
                        Log.debug("switch to audio track " + _current_track + ", load Playlist");
                    }
                    clearTimeout(_timeoutID);
                    _timeoutID = setTimeout(_loadAudioLevelPlaylist, 0);
                }
            }
        };

        private function _close() : void {
            CONFIG::LOGGING {
                Log.debug("cancel any audio level load in progress");
            }
            _closed = true;
            clearTimeout(_timeoutID);
            try {
                _urlloader.close();
                if (_manifest_loading) {
                    _manifest_loading.close();
                }
            } catch(e : Error) {
            }
        }

        /** When the framework idles out, stop reloading manifest **/
        private function _stateHandler(event : HLSEvent) : void {
            if (event.state == HLSPlayStates.IDLE) {
                _close();
            }
        };
    }
}

/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mangui.hls.loader {
    import org.mangui.hls.model.Level;

    import flash.utils.getTimer;
    import flash.utils.clearTimeout;
    import flash.utils.setTimeout;
    import flash.events.ErrorEvent;
    import flash.events.SecurityErrorEvent;
    import flash.events.IOErrorEvent;

    import org.mangui.hls.HLSSettings;
    import org.mangui.hls.constant.HLSPlayStates;
    import org.mangui.hls.model.AudioTrack;
    import org.mangui.hls.model.Fragment;
    import org.mangui.hls.event.HLSError;
    import org.mangui.hls.event.HLSEvent;
    import org.mangui.hls.playlist.AltAudioTrack;
    import org.mangui.hls.playlist.Manifest;

    import flash.net.URLLoader;

    import org.mangui.hls.HLS;

    public class AltAudioLevelLoader {
        CONFIG::LOGGING {
            import org.mangui.hls.utils.Log;
        }
        /** Reference to the hls framework controller. **/
        private var _hls : HLS;
        /** Object that fetches the manifest. **/
        private var _urlloader : URLLoader;
        /** Link to the M3U8 file. **/
        private var _url : String;
        /** Timeout ID for reloading live playlists. **/
        private var _timeoutID : uint;
        /** last reload manifest time **/
        private var _reload_playlists_timer : uint;
        /** current audio level **/
        private var _current_track : int;
        /** reference to manifest being loaded **/
        private var _manifest_loading : Manifest;
        /** is this loader closed **/
        private var _closed : Boolean = false;
        /* playlist retry timeout */
        private var _retry_timeout : Number;
        private var _retry_count : int;

        /** Setup the loader. **/
        public function AltAudioLevelLoader(hls : HLS) {
            _hls = hls;
            _hls.addEventListener(HLSEvent.PLAYBACK_STATE, _stateHandler);
            _hls.addEventListener(HLSEvent.AUDIO_TRACK_SWITCH, _audioTrackSwitchHandler);
        };

        public function dispose() : void {
            _close();
            _hls.removeEventListener(HLSEvent.PLAYBACK_STATE, _stateHandler);
            _hls.removeEventListener(HLSEvent.AUDIO_TRACK_SWITCH, _audioTrackSwitchHandler);
        }

        /** Loading failed; return errors. **/
        private function _errorHandler(event : ErrorEvent) : void {
            var txt : String;
            var code : int;
            if (event is SecurityErrorEvent) {
                code = HLSError.MANIFEST_LOADING_CROSSDOMAIN_ERROR;
                txt = "Cannot load M3U8: crossdomain access denied:" + event.text;
            } else if (event is IOErrorEvent && (HLSSettings.manifestLoadMaxRetry == -1 || _retry_count < HLSSettings.manifestLoadMaxRetry)) {
                CONFIG::LOGGING {
                    Log.warn("I/O Error while trying to load Playlist, retry in " + _retry_timeout + " ms");
                }
                _timeoutID = setTimeout(_loadAudioLevelPlaylist, _retry_timeout);
                /* exponential increase of retry timeout, capped to manifestLoadMaxRetryTimeout */
                _retry_timeout = Math.min(HLSSettings.manifestLoadMaxRetryTimeout, 2 * _retry_timeout);
                _retry_count++;
                return;
            } else {
                code = HLSError.MANIFEST_LOADING_IO_ERROR;
                txt = "Cannot load M3U8: " + event.text;
            }
            var hlsError : HLSError = new HLSError(code, _url, txt);
            _hls.dispatchEvent(new HLSEvent(HLSEvent.ERROR, hlsError));
        };

        /** parse a playlist **/
        private function _parseAudioPlaylist(string : String, url : String, level : int) : void {
            if (string != null && string.length != 0) {
                CONFIG::LOGGING {
                    Log.debug("audio level " + level + " playlist:\n" + string);
                }
                var frags : Vector.<Fragment> = Manifest.getFragments(string, url, level);
                // set fragment and update sequence number range
                var newLevel : Level = new Level();
                newLevel.updateFragments(frags);
                newLevel.targetduration = Manifest.getTargetDuration(string);
                _hls.audioTracks[_current_track].level = newLevel;
            }
            _hls.dispatchEvent(new HLSEvent(HLSEvent.AUDIO_LEVEL_LOADED, level));
            _manifest_loading = null;
        };

        /** load/reload active M3U8 playlist **/
        private function _loadAudioLevelPlaylist() : void {
            if (_closed) {
                return;
            }
            _reload_playlists_timer = getTimer();
            var altAudioTrack : AltAudioTrack = _hls.altAudioTracks[_hls.audioTracks[_current_track].id];
            _manifest_loading = new Manifest();
            _manifest_loading.loadPlaylist(altAudioTrack.url, _parseAudioPlaylist, _errorHandler, _current_track, _hls.type, HLSSettings.flushLiveURLCache);
            _hls.dispatchEvent(new HLSEvent(HLSEvent.AUDIO_LEVEL_LOADING, _current_track));
        };

        /** When audio track switch occurs, assess the need of loading audio level playlist **/
        private function _audioTrackSwitchHandler(event : HLSEvent) : void {
            _current_track = event.audioTrack;
            var audioTrack : AudioTrack = _hls.audioTracks[_current_track];
            if (audioTrack.source == AudioTrack.FROM_PLAYLIST) {
                var altAudioTrack : AltAudioTrack = _hls.altAudioTracks[audioTrack.id];
                if (altAudioTrack.url && audioTrack.level == null) {
                    CONFIG::LOGGING {
                        Log.debug("switch to audio track " + _current_track + ", load Playlist");
                    }
                    _retry_timeout = 1000;
                    _retry_count = 0;
                    clearTimeout(_timeoutID);
                    _timeoutID = setTimeout(_loadAudioLevelPlaylist, 0);
                }
            }
        };

        private function _close() : void {
            CONFIG::LOGGING {
                Log.debug("cancel any audio level load in progress");
            }
            _closed = true;
            clearTimeout(_timeoutID);
            try {
                _urlloader.close();
                if (_manifest_loading) {
                    _manifest_loading.close();
                }
            } catch(e : Error) {
            }
        }

        /** When the framework idles out, stop reloading manifest **/
        private function _stateHandler(event : HLSEvent) : void {
            if (event.state == HLSPlayStates.IDLE) {
                _close();
            }
        };
    }
}

set timeout to reload alt-audio playlists


/*
 * Copyright 2016 Google Inc. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package {

  import flash.display.Bitmap;
  import flash.display.BitmapData;
  import flash.display.BlendMode;
  import flash.display.Loader;
  import flash.display.Sprite;
  import flash.display.StageAlign;
  import flash.display.StageScaleMode;
  import flash.events.ErrorEvent;
  import flash.events.Event;
  import flash.events.IOErrorEvent;
  import flash.events.UncaughtErrorEvent;
  import flash.external.ExternalInterface;
  import flash.geom.Matrix;
  import flash.geom.Matrix3D;
  import flash.geom.PerspectiveProjection;
  import flash.geom.Point;
  import flash.geom.Rectangle;
  import flash.geom.Vector3D;
  import flash.net.URLRequest;
  import flash.system.Security;
  import flash.utils.Dictionary;

  public class FlashCommon extends Sprite {

    private var imageLoadedCallback:String = null;

    public function FlashCommon() {

      // Install event handler for uncaught errors.
      loaderInfo.uncaughtErrorEvents.addEventListener(
          UncaughtErrorEvent.UNCAUGHT_ERROR, handleError);

      // Declare external interface.
      ExternalInterface.marshallExceptions = true
      ExternalInterface.addCallback('isReady', isReady);
      ExternalInterface.addCallback('createLayer', createLayer);
      ExternalInterface.addCallback('destroyLayer', destroyLayer);
      ExternalInterface.addCallback('loadImage', loadImage);
      ExternalInterface.addCallback('cancelImage', cancelImage);
      ExternalInterface.addCallback('unloadImage', unloadImage);
      ExternalInterface.addCallback('createTexture', createTexture);
      ExternalInterface.addCallback('destroyTexture', destroyTexture);
      ExternalInterface.addCallback('drawCubeTiles', drawCubeTiles);
      ExternalInterface.addCallback('drawFlatTiles', drawFlatTiles);

      var callbacksObjName:String = loaderInfo.parameters.callbacksObjName as String;
      imageLoadedCallback = callbacksObjName + '.imageLoaded';

      Security.allowDomain("*");

      if (stage) {
        init();
      } else {
        addEventListener(Event.ADDED_TO_STAGE, init);
      }

    }

    private function handleError(event:UncaughtErrorEvent):void {
      var message:String;
      if (event.error is Error) {
        message = Error(event.error).message;
      } else if (event.error is ErrorEvent) {
        message = ErrorEvent(event.error).text;
      } else {
        message = event.error.toString();
      }
      debug('ERROR: ' + message);
    };

    private function debug(str:String):void {
      ExternalInterface.call('console.log', str);
    }

    public function isReady():Boolean {
      return true;
    }

    private function init():void {
      removeEventListener(Event.ADDED_TO_STAGE, init);
      stage.scaleMode = StageScaleMode.NO_SCALE;
      stage.align = StageAlign.TOP_LEFT;
      stage.frameRate = 60;
    }

    private function convertFov(fov:Number, thisDimension:Number, otherDimension:Number):Number {
      return 2 * Math.atan(otherDimension * Math.tan(fov / 2) / thisDimension);
    }

    private function getLayer(id:Number):Sprite {
      if (id in layerMap) {
        return layerMap[id];
      }
      return null;
    }

    public function createLayer(id: Number):void {
      if (id in layerMap) {
        debug('createLayer: ' + id + ' already exists');
        return;
      }

      var layer:Sprite = new Sprite();

      layerMap[id] = layer;
      layer.transform.perspectiveProjection = new PerspectiveProjection();
      layer.blendMode = BlendMode.LAYER;
      addChild(layer);
    }

    public function destroyLayer(id:Number):void {
      if (!(id in layerMap)) {
        debug('destroyLayer: ' + id + ' does not exist');
        return;
      }
      var layer:Sprite = layerMap[id];

      while (layer.numChildren > 0) {
        layer.removeChildAt(0);
      }
      removeChild(layer);
      delete layerMap[id];
    }

    public function drawCubeTiles(layerId:Number, width:Number, height:Number, left:Number, top:Number, alpha:Number, yaw:Number, pitch:Number, roll:Number, fov:Number, tiles:Array):void {
      var layer:Sprite = getLayer(layerId);
      if (!layer) {
        debug('drawCubeTiles: layer ' + layerId + ' does not exist');
      }

      // Remove all current children.
      while (layer.numChildren > 0) {
        layer.removeChildAt(0);
      }

      // Set viewport.
      layer.x = left;
      layer.y = top;
      layer.scrollRect = new Rectangle(0, 0, width, height);

      // Set opacity.
      layer.alpha = alpha;

      // Set fov.
      // Don't really know why this needs to be done; magic number adjusted from
      // https://github.com/fieldOfView/CuTy/blob/master/com/fieldofview/cuty/CutyScene.as#L260
      var fieldOfView:Number = convertFov(fov, 1, 500/height);
      layer.transform.perspectiveProjection.fieldOfView = fieldOfView * 180/Math.PI;

      var projectionCenter:Point = new Point(width/2, height/2);
      layer.transform.perspectiveProjection.projectionCenter = projectionCenter;

      var focalLength:Number = layer.transform.perspectiveProjection.focalLength;

      var viewportCenter:Vector3D = new Vector3D(width/2, height/2, 0);
      var focalCenter:Vector3D = new Vector3D(width/2, height/2, -focalLength);

      for each (var t:Object in tiles) {
        var texture:Sprite = textureMap[t.textureId];
        if (!texture) {
          debug('drawCubeTiles: texture ' + t.textureId + ' does not exist');
        }

        var m:Matrix3D = new Matrix3D();

        // Set the perspective depth.
        m.appendTranslation(0, 0, -focalLength);

        // Center tile in viewport.
        var offsetX:Number = width/2 - t.width/2;
        var offsetY:Number = height/2 - t.height/2;
        m.appendTranslation(offsetX, offsetY, 0);

        // Set tile offset within cube face.
        var translX:Number = t.centerX * t.levelSize - t.padLeft;
        var translY:Number = -t.centerY * t.levelSize - t.padTop;
        var translZ:Number = t.levelSize / 2;
        m.appendTranslation(translX, translY, translZ);

        // Set cube face rotation.
        m.appendRotation(-t.rotY, Vector3D.Y_AXIS, focalCenter);
        m.appendRotation(t.rotX, Vector3D.X_AXIS, focalCenter);

        // Set camera rotation.
        var rotX:Number = pitch*180/Math.PI;
        var rotY:Number = -yaw*180/Math.PI;
        var rotZ:Number = -roll*180/Math.PI;
        m.appendRotation(rotY, Vector3D.Y_AXIS, focalCenter);
        m.appendRotation(rotX, Vector3D.X_AXIS, focalCenter);
        m.appendRotation(rotZ, Vector3D.Z_AXIS, focalCenter);

        texture.transform.matrix3D = m;

        layer.addChild(texture);
      }
    }

    public function drawFlatTiles(layerId:Number, width:Number, height:Number, left:Number, top:Number, alpha:Number, x:Number, y:Number, zoomX:Number, zoomY:Number, tiles:Array):void {
      var layer:Sprite = getLayer(layerId);
      if (!layer) {
        debug('drawFlatTiles: layer ' + layerId + ' does not exist');
      }

      // Remove all current children.
      while (layer.numChildren > 0) {
        layer.removeChildAt(0);
      }

      // Set viewport.
      layer.x = left;
      layer.y = top;
      layer.scrollRect = new Rectangle(0, 0, width, height);

      // Set opacity.
      layer.alpha = alpha;

      // Determine the zoom factor.
      zoomX = width / zoomX;
      zoomY = height / zoomY;

      for each (var t:Object in tiles) {
        var texture:Sprite = textureMap[t.textureId];
        if (!texture) {
          debug('drawFlatTiles: texture ' + t.textureId + ' does not exist');
        }

        var m:Matrix3D = new Matrix3D();

        // Scale tile into correct size.
        var scaleX:Number = zoomX / t.levelWidth;
        var scaleY:Number = zoomY / t.levelHeight;
        m.appendScale(scaleX, scaleY, 1);

        // Place top left corner of tile at the center of the viewport.
        var offsetX:Number = width/2;
        var offsetY:Number = height/2;
        m.appendTranslation(offsetX, offsetY, 0);

        // Move tile into its position within the image.
        var cornerX:Number = t.centerX - t.scaleX / 2 + 0.5;
        var cornerY:Number = 0.5 - t.centerY - t.scaleY / 2;
        var posX:Number = cornerX * zoomX;
        var posY:Number = cornerY * zoomY;
        m.appendTranslation(posX, posY, 0);

        // Compensate for padding around the tile.
        m.appendTranslation(-t.padLeft, -t.padTop, 0);

        // Apply view offsets.
        var translX:Number = -x * zoomX;
        var translY:Number = -y * zoomY;
        m.appendTranslation(translX, translY, 0);

        texture.transform.matrix3D = m;

        layer.addChild(texture);
      }
    }

    private var imageMap:Dictionary = new Dictionary();
    private var textureMap:Dictionary = new Dictionary();
    private var layerMap:Dictionary = new Dictionary();

    private var nextId:Number = 1;

    public function loadImage(url:String, width:Number, height:Number, x:Number, y:Number):Number {
      var id:Number = nextId++;

      var loader:Loader = new Loader();
      imageMap[id] = loader;

      // TODO: Check if there are there other kinds of errors we can catch.
      loader.contentLoaderInfo.addEventListener(Event.COMPLETE, loadSuccess);
      loader.contentLoaderInfo.addEventListener(IOErrorEvent.IO_ERROR, loadError);

      var urlRequest:URLRequest = new URLRequest(url);
      loader.load(urlRequest);

      return id;

      function loadSuccess(e:Event):void {
        if (!imageMap[id]) {
          // Probably canceled before load was complete; ignore.
          return;
        }

        var image:Bitmap = Bitmap(loader.content);

        // Convert relative offset/size to absolute values.
        x *= image.bitmapData.width;
        y *= image.bitmapData.height;
        width *= image.bitmapData.width;
        height *= image.bitmapData.height;

        var croppedData:BitmapData = new BitmapData(width, height);
        croppedData.copyPixels(image.bitmapData, new Rectangle(x, y, width, height), new Point(0,0));

        imageMap[id] = croppedData;

        ExternalInterface.call(imageLoadedCallback, false, id);
      }

      function loadError(e:IOErrorEvent):void {
        if (!imageMap[id]) {
          // Probably canceled before load was complete; ignore.
          return;
        }
        delete imageMap[id];
        ExternalInterface.call(imageLoadedCallback, true, id);
      }
    }

    public function cancelImage(id:Number):void {
      if (!imageMap[id]) {
        debug('cancelImage: image ' + id + ' does not exist');
        return;
      }
      unloadImage(id);
    }

    public function unloadImage(id:Number):void {
      var loaderOrBitmapData:Object = imageMap[id];
      if (!loaderOrBitmapData) {
        debug('unloadImage: image ' + id + ' does not exist');
        return;
      }
      if (loaderOrBitmapData is Loader) {
        var loader:Loader = loaderOrBitmapData as Loader;
        // Calling close() after the load is complete seems to cause an error,
        // so first check that we are still loading.
        if (loader.contentLoaderInfo.bytesTotal != 0 && loader.contentLoaderInfo.bytesLoaded != loader.contentLoaderInfo.bytesTotal) {
          loader.close();
        }
      }
      delete imageMap[id];
    }

    public function createTexture(imageId:Number, width:Number, height:Number, padTop:Number, padBottom:Number, padLeft:Number, padRight:Number):Number {
      var image:BitmapData = imageMap[imageId];

      var i:Number;
      var point:Point = new Point();
      var rect:Rectangle = new Rectangle();

      // Create new bitmap for texture.
      var bitmap:BitmapData = new BitmapData(width + padLeft + padRight, height + padTop + padBottom, true, 0x00FFFFFF);

      // Resize source image if the texture has a different size.
      // TODO: it would be better to call draw() directly on the final bitmap
      // with both a matrix and a clippingRect parameter; this would both be
      // faster and allocate less memory. However, the clippingRect parameter
      // for the draw() method doesn't seem to work as documented.
      var scaled:BitmapData;
      if (width !== image.width || height !== image.height) {
        scaled = new BitmapData(width, height, true, 0x00FFFFFF);
        var mat:Matrix = new Matrix(width / image.width, 0, 0, height / image.height, 0, 0);
        scaled.draw(image, mat, null, null, null, true);
      } else {
        scaled = image;
      }

      // Draw image into texture.
      rect.x = 0;
      rect.y = 0;
      rect.width = width;
      rect.height = height;

      point.x = padLeft;
      point.y = padTop;
      bitmap.copyPixels(scaled, rect, point);

      // Draw top padding.
      for (i = 0; i <= padTop; i++) {
        rect.x = padLeft;
        rect.y = padTop;
        rect.width = width;
        rect.height = 1;

        point.x = padLeft;
        point.y = i;
        bitmap.copyPixels(bitmap, rect, point);
      }

      // Draw left padding.
      for (i = 0; i <= padLeft; i++) {
        rect.x = padLeft;
        rect.y = padTop;
        rect.width = 1;
        rect.height = height;

        point.x = i;
        point.y = padTop;
        bitmap.copyPixels(bitmap, rect, point);
      }

      // Draw bottom padding.
      for (i = 0; i <= padBottom; i++) {
        rect.x = padLeft;
        rect.y = padTop + height - 1;
        rect.width = width;
        rect.height = 1;

        point.x = padLeft;
        point.y = padTop + height + i;
        bitmap.copyPixels(bitmap, rect, point);
      }

      // Draw right padding.
      for (i = 0; i <= padRight; i++) {
        rect.x = padLeft + width - 1;
        rect.y = padTop;
        rect.width = 1;
        rect.height = height;

        point.x = padLeft + width + i;
        point.y = padTop;
        bitmap.copyPixels(bitmap, rect, point);
      }

      var texture:Sprite = new Sprite();
      texture.addChild(new Bitmap(bitmap));

      var textureId:Number = imageId;
      textureMap[textureId] = texture;

      return textureId;
    }

    public function destroyTexture(id:Number):void {
      var texture:Sprite = textureMap[id];
      if (!texture) {
        debug('destroyTexture: texture ' + id + ' does not exist');
        return;
      }
      if (texture.parent) {
        texture.parent.removeChild(texture);
      }
      delete textureMap[id];
    }
  }
}

/*
 * Copyright 2016 Google Inc. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package {

  import flash.display.Bitmap;
  import flash.display.BitmapData;
  import flash.display.BlendMode;
  import flash.display.Loader;
  import flash.display.Sprite;
  import flash.display.StageAlign;
  import flash.display.StageScaleMode;
  import flash.events.ErrorEvent;
  import flash.events.Event;
  import flash.events.IOErrorEvent;
  import flash.events.UncaughtErrorEvent;
  import flash.external.ExternalInterface;
  import flash.geom.Matrix;
  import flash.geom.Matrix3D;
  import flash.geom.PerspectiveProjection;
  import flash.geom.Point;
  import flash.geom.Rectangle;
  import flash.geom.Vector3D;
  import flash.net.URLRequest;
  import flash.system.LoaderContext;
  import flash.system.Security;
  import flash.utils.Dictionary;

  public class FlashCommon extends Sprite {

    private var imageLoadedCallback:String = null;

    public function FlashCommon() {

      // Install event handler for uncaught errors.
      loaderInfo.uncaughtErrorEvents.addEventListener(
          UncaughtErrorEvent.UNCAUGHT_ERROR, handleError);

      // Declare external interface.
      ExternalInterface.marshallExceptions = true
      ExternalInterface.addCallback('isReady', isReady);
      ExternalInterface.addCallback('createLayer', createLayer);
      ExternalInterface.addCallback('destroyLayer', destroyLayer);
      ExternalInterface.addCallback('loadImage', loadImage);
      ExternalInterface.addCallback('cancelImage', cancelImage);
      ExternalInterface.addCallback('unloadImage', unloadImage);
      ExternalInterface.addCallback('createTexture', createTexture);
      ExternalInterface.addCallback('destroyTexture', destroyTexture);
      ExternalInterface.addCallback('drawCubeTiles', drawCubeTiles);
      ExternalInterface.addCallback('drawFlatTiles', drawFlatTiles);

      var callbacksObjName:String = loaderInfo.parameters.callbacksObjName as String;
      imageLoadedCallback = callbacksObjName + '.imageLoaded';

      Security.allowDomain("*");

      if (stage) {
        init();
      } else {
        addEventListener(Event.ADDED_TO_STAGE, init);
      }

    }

    private function handleError(event:UncaughtErrorEvent):void {
      var message:String;
      if (event.error is Error) {
        message = Error(event.error).message;
      } else if (event.error is ErrorEvent) {
        message = ErrorEvent(event.error).text;
      } else {
        message = event.error.toString();
      }
      debug('ERROR: ' + message);
    };

    private function debug(str:String):void {
      ExternalInterface.call('console.log', str);
    }

    public function isReady():Boolean {
      return true;
    }

    private function init():void {
      removeEventListener(Event.ADDED_TO_STAGE, init);
      stage.scaleMode = StageScaleMode.NO_SCALE;
      stage.align = StageAlign.TOP_LEFT;
      stage.frameRate = 60;
    }

    private function convertFov(fov:Number, thisDimension:Number, otherDimension:Number):Number {
      return 2 * Math.atan(otherDimension * Math.tan(fov / 2) / thisDimension);
    }

    private function getLayer(id:Number):Sprite {
      if (id in layerMap) {
        return layerMap[id];
      }
      return null;
    }

    public function createLayer(id: Number):void {
      if (id in layerMap) {
        debug('createLayer: ' + id + ' already exists');
        return;
      }

      var layer:Sprite = new Sprite();

      layerMap[id] = layer;
      layer.transform.perspectiveProjection = new PerspectiveProjection();
      layer.blendMode = BlendMode.LAYER;
      addChild(layer);
    }

    public function destroyLayer(id:Number):void {
      if (!(id in layerMap)) {
        debug('destroyLayer: ' + id + ' does not exist');
        return;
      }
      var layer:Sprite = layerMap[id];

      while (layer.numChildren > 0) {
        layer.removeChildAt(0);
      }
      removeChild(layer);
      delete layerMap[id];
    }

    public function drawCubeTiles(layerId:Number, width:Number, height:Number, left:Number, top:Number, alpha:Number, yaw:Number, pitch:Number, roll:Number, fov:Number, tiles:Array):void {
      var layer:Sprite = getLayer(layerId);
      if (!layer) {
        debug('drawCubeTiles: layer ' + layerId + ' does not exist');
      }

      // Remove all current children.
      while (layer.numChildren > 0) {
        layer.removeChildAt(0);
      }

      // Set viewport.
      layer.x = left;
      layer.y = top;
      layer.scrollRect = new Rectangle(0, 0, width, height);

      // Set opacity.
      layer.alpha = alpha;

      // Set fov.
      // Don't really know why this needs to be done; magic number adjusted from
      // https://github.com/fieldOfView/CuTy/blob/master/com/fieldofview/cuty/CutyScene.as#L260
      var fieldOfView:Number = convertFov(fov, 1, 500/height);
      layer.transform.perspectiveProjection.fieldOfView = fieldOfView * 180/Math.PI;

      var projectionCenter:Point = new Point(width/2, height/2);
      layer.transform.perspectiveProjection.projectionCenter = projectionCenter;

      var focalLength:Number = layer.transform.perspectiveProjection.focalLength;

      var viewportCenter:Vector3D = new Vector3D(width/2, height/2, 0);
      var focalCenter:Vector3D = new Vector3D(width/2, height/2, -focalLength);

      for each (var t:Object in tiles) {
        var texture:Sprite = textureMap[t.textureId];
        if (!texture) {
          debug('drawCubeTiles: texture ' + t.textureId + ' does not exist');
        }

        var m:Matrix3D = new Matrix3D();

        // Set the perspective depth.
        m.appendTranslation(0, 0, -focalLength);

        // Center tile in viewport.
        var offsetX:Number = width/2 - t.width/2;
        var offsetY:Number = height/2 - t.height/2;
        m.appendTranslation(offsetX, offsetY, 0);

        // Set tile offset within cube face.
        var translX:Number = t.centerX * t.levelSize - t.padLeft;
        var translY:Number = -t.centerY * t.levelSize - t.padTop;
        var translZ:Number = t.levelSize / 2;
        m.appendTranslation(translX, translY, translZ);

        // Set cube face rotation.
        m.appendRotation(-t.rotY, Vector3D.Y_AXIS, focalCenter);
        m.appendRotation(t.rotX, Vector3D.X_AXIS, focalCenter);

        // Set camera rotation.
        var rotX:Number = pitch*180/Math.PI;
        var rotY:Number = -yaw*180/Math.PI;
        var rotZ:Number = -roll*180/Math.PI;
        m.appendRotation(rotY, Vector3D.Y_AXIS, focalCenter);
        m.appendRotation(rotX, Vector3D.X_AXIS, focalCenter);
        m.appendRotation(rotZ, Vector3D.Z_AXIS, focalCenter);

        texture.transform.matrix3D = m;

        layer.addChild(texture);
      }
    }

    public function drawFlatTiles(layerId:Number, width:Number, height:Number, left:Number, top:Number, alpha:Number, x:Number, y:Number, zoomX:Number, zoomY:Number, tiles:Array):void {
      var layer:Sprite = getLayer(layerId);
      if (!layer) {
        debug('drawFlatTiles: layer ' + layerId + ' does not exist');
      }

      // Remove all current children.
      while (layer.numChildren > 0) {
        layer.removeChildAt(0);
      }

      // Set viewport.
      layer.x = left;
      layer.y = top;
      layer.scrollRect = new Rectangle(0, 0, width, height);

      // Set opacity.
      layer.alpha = alpha;

      // Determine the zoom factor.
      zoomX = width / zoomX;
      zoomY = height / zoomY;

      for each (var t:Object in tiles) {
        var texture:Sprite = textureMap[t.textureId];
        if (!texture) {
          debug('drawFlatTiles: texture ' + t.textureId + ' does not exist');
        }

        var m:Matrix3D = new Matrix3D();

        // Scale tile into correct size.
        var scaleX:Number = zoomX / t.levelWidth;
        var scaleY:Number = zoomY / t.levelHeight;
        m.appendScale(scaleX, scaleY, 1);

        // Place top left corner of tile at the center of the viewport.
        var offsetX:Number = width/2;
        var offsetY:Number = height/2;
        m.appendTranslation(offsetX, offsetY, 0);

        // Move tile into its position within the image.
        var cornerX:Number = t.centerX - t.scaleX / 2 + 0.5;
        var cornerY:Number = 0.5 - t.centerY - t.scaleY / 2;
        var posX:Number = cornerX * zoomX;
        var posY:Number = cornerY * zoomY;
        m.appendTranslation(posX, posY, 0);

        // Compensate for padding around the tile.
        m.appendTranslation(-t.padLeft, -t.padTop, 0);

        // Apply view offsets.
        var translX:Number = -x * zoomX;
        var translY:Number = -y * zoomY;
        m.appendTranslation(translX, translY, 0);

        texture.transform.matrix3D = m;

        layer.addChild(texture);
      }
    }

    private var imageMap:Dictionary = new Dictionary();
    private var textureMap:Dictionary = new Dictionary();
    private var layerMap:Dictionary = new Dictionary();

    private var nextId:Number = 1;

    public function loadImage(url:String, width:Number, height:Number, x:Number, y:Number):Number {
      var id:Number = nextId++;

      var loader:Loader = new Loader();
      imageMap[id] = loader;

      loader.contentLoaderInfo.addEventListener(Event.COMPLETE, loadSuccess);
      loader.contentLoaderInfo.addEventListener(IOErrorEvent.IO_ERROR, loadError);

      // Check the server policy file for cross-domain requests.
      var loaderContext:LoaderContext = new LoaderContext();
      loaderContext.checkPolicyFile = true;

      var urlRequest:URLRequest = new URLRequest(url);
      loader.load(urlRequest, loaderContext);

      return id;

      function loadSuccess(e:Event):void {
        if (!imageMap[id]) {
          // Probably canceled before load was complete; ignore.
          return;
        }

        var image:Bitmap = Bitmap(loader.content);

        // Convert relative offset/size to absolute values.
        x *= image.bitmapData.width;
        y *= image.bitmapData.height;
        width *= image.bitmapData.width;
        height *= image.bitmapData.height;

        var croppedData:BitmapData = new BitmapData(width, height);
        croppedData.copyPixels(image.bitmapData, new Rectangle(x, y, width, height), new Point(0,0));

        imageMap[id] = croppedData;

        ExternalInterface.call(imageLoadedCallback, false, id);
      }

      function loadError(e:IOErrorEvent):void {
        if (!imageMap[id]) {
          // Probably canceled before load was complete; ignore.
          return;
        }
        delete imageMap[id];
        ExternalInterface.call(imageLoadedCallback, true, id);
      }
    }

    public function cancelImage(id:Number):void {
      if (!imageMap[id]) {
        debug('cancelImage: image ' + id + ' does not exist');
        return;
      }
      unloadImage(id);
    }

    public function unloadImage(id:Number):void {
      var loaderOrBitmapData:Object = imageMap[id];
      if (!loaderOrBitmapData) {
        debug('unloadImage: image ' + id + ' does not exist');
        return;
      }
      if (loaderOrBitmapData is Loader) {
        var loader:Loader = loaderOrBitmapData as Loader;
        // Calling close() after the load is complete seems to cause an error,
        // so first check that we are still loading.
        if (loader.contentLoaderInfo.bytesTotal != 0 && loader.contentLoaderInfo.bytesLoaded != loader.contentLoaderInfo.bytesTotal) {
          loader.close();
        }
      }
      delete imageMap[id];
    }

    public function createTexture(imageId:Number, width:Number, height:Number, padTop:Number, padBottom:Number, padLeft:Number, padRight:Number):Number {
      var image:BitmapData = imageMap[imageId];

      var i:Number;
      var point:Point = new Point();
      var rect:Rectangle = new Rectangle();

      // Create new bitmap for texture.
      var bitmap:BitmapData = new BitmapData(width + padLeft + padRight, height + padTop + padBottom, true, 0x00FFFFFF);

      // Resize source image if the texture has a different size.
      // TODO: it would be better to call draw() directly on the final bitmap
      // with both a matrix and a clippingRect parameter; this would both be
      // faster and allocate less memory. However, the clippingRect parameter
      // for the draw() method doesn't seem to work as documented.
      var scaled:BitmapData;
      if (width !== image.width || height !== image.height) {
        scaled = new BitmapData(width, height, true, 0x00FFFFFF);
        var mat:Matrix = new Matrix(width / image.width, 0, 0, height / image.height, 0, 0);
        scaled.draw(image, mat, null, null, null, true);
      } else {
        scaled = image;
      }

      // Draw image into texture.
      rect.x = 0;
      rect.y = 0;
      rect.width = width;
      rect.height = height;

      point.x = padLeft;
      point.y = padTop;
      bitmap.copyPixels(scaled, rect, point);

      // Draw top padding.
      for (i = 0; i <= padTop; i++) {
        rect.x = padLeft;
        rect.y = padTop;
        rect.width = width;
        rect.height = 1;

        point.x = padLeft;
        point.y = i;
        bitmap.copyPixels(bitmap, rect, point);
      }

      // Draw left padding.
      for (i = 0; i <= padLeft; i++) {
        rect.x = padLeft;
        rect.y = padTop;
        rect.width = 1;
        rect.height = height;

        point.x = i;
        point.y = padTop;
        bitmap.copyPixels(bitmap, rect, point);
      }

      // Draw bottom padding.
      for (i = 0; i <= padBottom; i++) {
        rect.x = padLeft;
        rect.y = padTop + height - 1;
        rect.width = width;
        rect.height = 1;

        point.x = padLeft;
        point.y = padTop + height + i;
        bitmap.copyPixels(bitmap, rect, point);
      }

      // Draw right padding.
      for (i = 0; i <= padRight; i++) {
        rect.x = padLeft + width - 1;
        rect.y = padTop;
        rect.width = 1;
        rect.height = height;

        point.x = padLeft + width + i;
        point.y = padTop;
        bitmap.copyPixels(bitmap, rect, point);
      }

      var texture:Sprite = new Sprite();
      texture.addChild(new Bitmap(bitmap));

      var textureId:Number = imageId;
      textureMap[textureId] = texture;

      return textureId;
    }

    public function destroyTexture(id:Number):void {
      var texture:Sprite = textureMap[id];
      if (!texture) {
        debug('destroyTexture: texture ' + id + ' does not exist');
        return;
      }
      if (texture.parent) {
        texture.parent.removeChild(texture);
      }
      delete textureMap[id];
    }
  }
}

Fix cross-domain image loading on Flash.

In order to programmatically access the pixels of cross-domain images,
Flash requires us to pass a LoaderContext with the checkPolicyFile
option set into the loader. Otherwise, cross-domain image requests
made by Flash will fail with a cryptic error.


//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2008 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.crowd.chat.data {

import com.threerings.io.SimpleStreamableObject;
import com.threerings.util.Comparable;
import com.threerings.util.Equalable;

import com.threerings.presents.dobj.DSet_Entry;

/**
 * Represents a chat channel.
 */
public /*abstract*/ class ChatChannel extends SimpleStreamableObject
    implements Comparable, Equalable, DSet_Entry
{
    // from interface Comparable
    public function compareTo (other :Object) :int
    {
        throw new Error("abstract");
    }

    // from interface Equalable
    public function equals (other :Object) :Boolean
    {
        throw new Error("abstract");
    }

    // from interface DSet_Entry
    public function getKey () :Object
    {
        return this;
    }
}
}

//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2008 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.crowd.chat.data {

import com.threerings.io.SimpleStreamableObject;
import com.threerings.util.Comparable;
import com.threerings.util.Equalable;

import com.threerings.presents.dobj.DSet_Entry;

/**
 * Represents a chat channel.
 */
public /*abstract*/ class ChatChannel extends SimpleStreamableObject
    implements Comparable, Equalable, DSet_Entry
{
    // from interface Comparable
    public function compareTo (other :Object) :int
    {
        throw new Error("abstract");
    }

    // from interface Equalable
    public function equals (other :Object) :Boolean
    {
        return compareTo(other) == 0;
    }

    // from interface DSet_Entry
    public function getKey () :Object
    {
        return this;
    }
}
}

Implement equals() with compareTo().

Implement equals() with compareTo().

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


package org.jivesoftware.xiff.core
{
	import flash.events.ErrorEvent;
	import flash.events.TimerEvent;
	import flash.utils.Timer;
	import flash.xml.XMLDocument;
	import flash.xml.XMLNode;

	import mx.rpc.events.FaultEvent;
	import mx.rpc.events.ResultEvent;
	import mx.rpc.http.HTTPService;

	import org.jivesoftware.xiff.auth.Anonymous;
	import org.jivesoftware.xiff.auth.External;
	import org.jivesoftware.xiff.auth.Plain;
	import org.jivesoftware.xiff.auth.SASLAuth;
	import org.jivesoftware.xiff.data.IQ;
	import org.jivesoftware.xiff.data.bind.BindExtension;
	import org.jivesoftware.xiff.data.session.SessionExtension;
	import org.jivesoftware.xiff.events.ConnectionSuccessEvent;
	import org.jivesoftware.xiff.events.IncomingDataEvent;
	import org.jivesoftware.xiff.events.LoginEvent;
	import org.jivesoftware.xiff.events.XIFFErrorEvent;
	import org.jivesoftware.xiff.util.Callback;

	public class XMPPBOSHConnection extends XMPPConnection
	{
		private static var saslMechanisms:Object = {
			"PLAIN":Plain,
			"ANONYMOUS":Anonymous,
            "EXTERNAL":External
		};

		private var maxConcurrentRequests:uint = 2;
		private var boshVersion:Number = 1.6;
		private var headers:Object = {
			"post": [],
			"get": ['Cache-Control', 'no-store', 'Cache-Control', 'no-cache', 'Pragma', 'no-cache']
		};
		private var requestCount:int = 0;
		private var failedRequests:Array = [];
		private var requestQueue:Array = [];
		private var responseQueue:Array = [];
		private const responseTimer:Timer = new Timer(0.0, 1);
		private var isDisconnecting:Boolean = false;
		private var boshPollingInterval:Number = 10000;
		private var sid:String;
		private var rid:Number;
		private var wait:int;
		private var inactivity:int;
		private var pollTimer:Timer;
		private var isCurrentlyPolling:Boolean = false;

		private var auth:SASLAuth;
		private var authHandler:Function;
		private var https:Boolean = false;
		private var _port:Number = -1;

		private var callbacks:Object = {};

		public static var logger:Object = null;

		public override function connect(streamType:String=null):Boolean
		{
			if(logger)
				logger.log("CONNECT()", "INFO");
			BindExtension.enable();
			active = false;
			loggedIn = false;
			var attrs:Object = {
	            xmlns: "http://jabber.org/protocol/httpbind",
	            hold: maxConcurrentRequests,
	            rid: nextRID,
	            secure: https,
	            wait: 10,
	            ver: boshVersion
	        }

	        var result:XMLNode = new XMLNode(1, "body");
	        result.attributes = attrs;
	        sendRequests(result);

	        return true;
		}

		public static function registerSASLMechanism(name:String, authClass:Class):void
		{
			saslMechanisms[name] = authClass;
		}

		public static function disableSASLMechanism(name:String):void
		{
			saslMechanisms[name] = null;
		}

		public function set secure(flag:Boolean):void
		{
			https = flag;
		}

		public override function set port(portnum:Number):void
		{
			_port = portnum;
		}

		public override function get port():Number
		{
			if(_port == -1)
				return https ? 8483 : 8080;
			return _port;
		}

		public function get httpServer():String
		{
			return (https ? "https" : "http") + "://" + server + ":" + port + "/http-bind/";
		}

		public override function disconnect():void
		{
			super.disconnect();
			pollTimer.stop();
			pollTimer = null;
		}

		public function processConnectionResponse(responseBody:XMLNode):void
		{
			var attributes:Object = responseBody.attributes;
			sid = attributes.sid;
	        boshPollingInterval = attributes.polling * 1000;
	        // if we have a polling interval of 1, we want to add an extra second for a little bit of
	        // padding.
	        if(boshPollingInterval <= 1000 && boshPollingInterval > 0)
	            boshPollingInterval += 1000;

	        wait = attributes.wait;
	        inactivity = attributes.inactivity;
	        var serverRequests:int = attributes.requests;
	        if (serverRequests)
	            maxConcurrentRequests = Math.min(serverRequests, maxConcurrentRequests);
	        active = true;
	        configureConnection(responseBody);
	        responseTimer.addEventListener(TimerEvent.TIMER_COMPLETE, processResponse);
	        pollTimer = new Timer(boshPollingInterval, 1);
	        pollTimer.addEventListener(TimerEvent.TIMER, pollServer);
		}

		private function processResponse(event:TimerEvent=null):void
		{
			// Read the data and send it to the appropriate parser
			var currentNode:XMLNode = responseQueue.shift();
			var nodeName:String = currentNode.nodeName.toLowerCase();

			switch( nodeName )
			{
				case "stream:features":
					//we already handled this in httpResponse()
					break;

				case "stream:error":
					handleStreamError( currentNode );
					break;

				case "iq":
					handleIQ( currentNode );
					break;

				case "message":
					handleMessage( currentNode );
					break;

				case "presence":
					handlePresence( currentNode );
					break;

				case "success":
					handleAuthentication( currentNode );
					break;
				default:
					dispatchError( "undefined-condition", "Unknown Error", "modify", 500 );
					break;
			}

			resetResponseProcessor();
		}

		private function resetResponseProcessor():void
		{
			if(responseQueue.length > 0)
			{
				responseTimer.reset();
				responseTimer.start();
			}
		}

		private function httpResponse(req:XMLNode, isPollResponse:Boolean, evt:ResultEvent):void
		{
			requestCount--;
			var rawXML:String = evt.result as String;

			if(logger)
	       		logger.log(rawXML, "INCOMING");

			var xmlData:XMLDocument = new XMLDocument();
			xmlData.ignoreWhite = this.ignoreWhite;
			xmlData.parseXML( rawXML );
			var bodyNode:XMLNode = xmlData.firstChild;

			var event:IncomingDataEvent = new IncomingDataEvent();
			event.data = xmlData;
			dispatchEvent( event );

			if(bodyNode.attributes["type"] == "terminal")
			{
				dispatchError( "BOSH Error", bodyNode.attributes["condition"], "", -1 );
				active = false;
			}

			for each(var childNode:XMLNode in bodyNode.childNodes)
			{
				if(childNode.nodeName == "stream:features")
				{
					_expireTagSearch = false;
					processConnectionResponse( bodyNode );
				}
				else
					responseQueue.push(childNode);
			}

			resetResponseProcessor();

			//if we just processed a poll response, we're no longer in mid-poll
			if(isPollResponse)
				isCurrentlyPolling = false;

			//if we have queued requests we want to send them before attempting to poll again
			//otherwise, if we don't already have a countdown going for the next poll, start one
	        if (!sendQueuedRequests() && (!pollTimer || !pollTimer.running))
	        	resetPollTimer();
		}

		private function httpError(req:XMLNode, isPollResponse:Boolean, evt:FaultEvent):void
		{
			active = false;
			dispatchError("Unknown HTTP Error", evt.fault.rootCause.text, "", -1);
		}

		protected override function sendXML(body:*):void
		{
			sendQueuedRequests(body);
		}

		private function sendQueuedRequests(body:*=null):Boolean
		{
			if(body)
				requestQueue.push(body);
			else if(requestQueue.length == 0)
				return false;

			return sendRequests();
		}

		//returns true if any requests were sent
		private function sendRequests(data:XMLNode = null, isPoll:Boolean=false):Boolean
		{
			if(requestCount >= maxConcurrentRequests)
				return false;

			if(isPoll)
				trace("Polling at: " + new Date().getSeconds());

			requestCount++;

			if(!data)
			{
				if(isPoll)
				{
					data = createRequest();
				}
				else
				{
					var temp:Array = [];
					for(var i:uint = 0; i < 10 && requestQueue.length > 0; i++)
					{
						temp.push(requestQueue.shift());
					}
					data = createRequest(temp);
				}
			}

			//build the http request
	        var request:HTTPService = new HTTPService();
	        request.method = "post";
	        request.headers = headers[request.method];
	        request.url = httpServer;
	        request.resultFormat = HTTPService.RESULT_FORMAT_TEXT;
	        request.contentType = "text/xml";

	        var responseCallback:Callback = new Callback(this, httpResponse, data, isPoll);
	        var errorCallback:Callback = new Callback(this, httpError, data, isPoll);

	        request.addEventListener(ResultEvent.RESULT, responseCallback.call, false);
	        request.addEventListener(FaultEvent.FAULT, errorCallback.call, false);

	       	if(logger)
	       		logger.log(data, "OUTGOING");

			request.send(data);

			resetPollTimer();

			return true;
		}

    	private function resetPollTimer():void
    	{
    		if(!pollTimer)
    			return;
    		pollTimer.reset();
    		pollTimer.start();
    	}

    	private function pollServer(evt:TimerEvent):void
    	{
    		//We shouldn't poll if the connection is dead, if we had requests to send instead, or if there's already one in progress
    		if(!isActive() || sendQueuedRequests() || isCurrentlyPolling)
    			return;

    		//this should be safe since sendRequests checks to be sure it's not over the concurrent requests limit, and we just ensured that the queue is empty by calling sendQueuedRequests()
    		isCurrentlyPolling = sendRequests(null, true);
    	}

    	private function get nextRID():Number {
    		if(!rid)
    			rid = Math.floor(Math.random() * 1000000);
    		return ++rid;
    	}

    	private function createRequest(bodyContent:Array=null):XMLNode {
    		var attrs:Object = {
    			xmlns: "http://jabber.org/protocol/httpbind",
				rid: nextRID,
				sid: sid
    		}
    		var req:XMLNode = new XMLNode(1, "body");
    		if(bodyContent)
    			for each(var content:XMLNode in bodyContent)
    					req.appendChild(content);

    		req.attributes = attrs;

    		return req;
    	}

	    private function handleAuthentication(responseBody:XMLNode):void
	    {
	    //    if(!response || response.length == 0) {
	     //       return;
	     //   }
	        var status:Object = auth.handleResponse(0, responseBody);
	        if (status.authComplete) {
	            if (status.authSuccess) {
	                bindConnection();
	            }
	            else {
	                dispatchEvent(new ErrorEvent(ErrorEvent.ERROR));
	                disconnect();
	            }
	        }
	    }

	    private function configureConnection(responseBody:XMLNode):void
	    {
	    	var features:XMLNode = responseBody.firstChild;

	        var authentication:Object = {};
	        for each(var feature:XMLNode in features.childNodes)
			{
	            if (feature.nodeName == "mechanisms")
	                authentication.auth = configureAuthMechanisms(feature);
	            else if (feature.nodeName == "bind")
	                authentication.bind = true;
	            else if (feature.nodeName == "session")
	                authentication.session = true;
	        }

	        auth = authentication.auth;
	        dispatchEvent(new ConnectionSuccessEvent());
	        authHandler = handleAuthentication;
	        sendXML(auth.request);
	    }

	    private function configureAuthMechanisms(mechanisms:XMLNode):SASLAuth
	    {
	        var authMechanism:SASLAuth;
	        var authClass:Class;
	        for each(var mechanism:XMLNode in mechanisms.childNodes)
	        {
	        	authClass = saslMechanisms[mechanism.firstChild.nodeValue];
	   			if(authClass)
	   				break;
	        }

	        if (!authClass) {
	        	dispatchError("SASL missing", "The server is not configured to support any available SASL mechanisms", "SASL", -1);
	        	return null;
	        }

	   		return new authClass(this);
	    }

	    public function handleBindResponse(packet:IQ):void
	    {
	    	var bind:BindExtension = packet.getExtension("bind") as BindExtension;

            var jid:String = bind.getJID();

            var parts:Array = jid.split("/");
            myResource = parts[1];
            parts = parts[0].split("@");
            myUsername = parts[0];
            domain = parts[1];

            establishSession();
	    }

	    private function bindConnection():void
	    {
	    	var bindIQ:IQ = new IQ(null, "set");

			var bindExt:BindExtension = new BindExtension();
			if(resource)
	        	bindExt.resource = resource;

	        bindIQ.addExtension(bindExt);

			//this is laaaaaame, it should be a function
			bindIQ.callbackName = "handleBindResponse";
			bindIQ.callbackScope = this;

	        send(bindIQ);
	    }

	    public function handleSessionResponse(packet:IQ):void
	    {
	    	dispatchEvent(new LoginEvent());
	    }

	    private function establishSession():void
	    {
	        var sessionIQ:IQ = new IQ(null, "set");

	        sessionIQ.addExtension(new SessionExtension());

	        sessionIQ.callbackName = "handleSessionResponse";
	        sessionIQ.callbackScope = this;

	        send(sessionIQ);
	    }

		//do nothing, we use polling instead
		public override function sendKeepAlive():void {}
	}
}

package org.jivesoftware.xiff.core
{
	import flash.events.ErrorEvent;
	import flash.events.TimerEvent;
	import flash.utils.Timer;
	import flash.xml.XMLDocument;
	import flash.xml.XMLNode;

	import mx.rpc.events.FaultEvent;
	import mx.rpc.events.ResultEvent;
	import mx.rpc.http.HTTPService;

	import org.jivesoftware.xiff.auth.Anonymous;
	import org.jivesoftware.xiff.auth.External;
	import org.jivesoftware.xiff.auth.Plain;
	import org.jivesoftware.xiff.auth.SASLAuth;
	import org.jivesoftware.xiff.data.IQ;
	import org.jivesoftware.xiff.data.bind.BindExtension;
	import org.jivesoftware.xiff.data.session.SessionExtension;
	import org.jivesoftware.xiff.events.ConnectionSuccessEvent;
	import org.jivesoftware.xiff.events.IncomingDataEvent;
	import org.jivesoftware.xiff.events.LoginEvent;
	import org.jivesoftware.xiff.events.XIFFErrorEvent;
	import org.jivesoftware.xiff.util.Callback;

	public class XMPPBOSHConnection extends XMPPConnection
	{
		private static var saslMechanisms:Object = {
			"PLAIN":Plain,
			"ANONYMOUS":Anonymous,
            "EXTERNAL":External
		};

		private var maxConcurrentRequests:uint = 2;
		private var boshVersion:Number = 1.6;
		private var headers:Object = {
			"post": [],
			"get": ['Cache-Control', 'no-store', 'Cache-Control', 'no-cache', 'Pragma', 'no-cache']
		};
		private var requestCount:int = 0;
		private var failedRequests:Array = [];
		private var requestQueue:Array = [];
		private var responseQueue:Array = [];
		private const responseTimer:Timer = new Timer(0.0, 1);
		private var isDisconnecting:Boolean = false;
		private var boshPollingInterval:Number = 10000;
		private var sid:String;
		private var rid:Number;
		private var wait:int;
		private var inactivity:int;
		private var pollTimer:Timer;
		private var isCurrentlyPolling:Boolean = false;

		private var auth:SASLAuth;
		private var authHandler:Function;
		private var https:Boolean = false;
		private var _port:Number = -1;

		private var callbacks:Object = {};

		public static var logger:Object = null;

		public override function connect(streamType:String=null):Boolean
		{
			if(logger)
				logger.log("CONNECT()", "INFO");
			BindExtension.enable();
			active = false;
			loggedIn = false;
			var attrs:Object = {
	            xmlns: "http://jabber.org/protocol/httpbind",
	            hold: maxConcurrentRequests,
	            rid: nextRID,
	            secure: https,
	            wait: 10,
	            ver: boshVersion
	        }

	        var result:XMLNode = new XMLNode(1, "body");
	        result.attributes = attrs;
	        sendRequests(result);

	        return true;
		}

		public static function registerSASLMechanism(name:String, authClass:Class):void
		{
			saslMechanisms[name] = authClass;
		}

		public static function disableSASLMechanism(name:String):void
		{
			saslMechanisms[name] = null;
		}

		public function set secure(flag:Boolean):void
		{
			https = flag;
		}

		public override function set port(portnum:Number):void
		{
			_port = portnum;
		}

		public override function get port():Number
		{
			if(_port == -1)
				return https ? 8483 : 8080;
			return _port;
		}

		public function get httpServer():String
		{
			return (https ? "https" : "http") + "://" + server + ":" + port + "/http-bind/";
		}

		public override function disconnect():void
		{
			super.disconnect();
			pollTimer.stop();
			pollTimer = null;
		}

		public function processConnectionResponse(responseBody:XMLNode):void
		{
			var attributes:Object = responseBody.attributes;
			sid = attributes.sid;
	        boshPollingInterval = attributes.polling * 1000;

	        wait = attributes.wait;
	        inactivity = attributes.inactivity * 1000;
	        if(inactivity - 2000 >= boshPollingInterval || (boshPollingInterval <= 1000 && boshPollingInterval > 0))
	        {
	        	boshPollingInterval += 1000;
	        }

	        var serverRequests:int = attributes.requests;
	        if (serverRequests)
	            maxConcurrentRequests = Math.min(serverRequests, maxConcurrentRequests);
	        active = true;
	        configureConnection(responseBody);
	        responseTimer.addEventListener(TimerEvent.TIMER_COMPLETE, processResponse);
	        pollTimer = new Timer(boshPollingInterval, 1);
	        pollTimer.addEventListener(TimerEvent.TIMER, pollServer);
		}

		private function processResponse(event:TimerEvent=null):void
		{
			// Read the data and send it to the appropriate parser
			var currentNode:XMLNode = responseQueue.shift();
			var nodeName:String = currentNode.nodeName.toLowerCase();

			switch( nodeName )
			{
				case "stream:features":
					//we already handled this in httpResponse()
					break;

				case "stream:error":
					handleStreamError( currentNode );
					break;

				case "iq":
					handleIQ( currentNode );
					break;

				case "message":
					handleMessage( currentNode );
					break;

				case "presence":
					handlePresence( currentNode );
					break;

				case "success":
					handleAuthentication( currentNode );
					break;
				default:
					dispatchError( "undefined-condition", "Unknown Error", "modify", 500 );
					break;
			}

			resetResponseProcessor();
		}

		private function resetResponseProcessor():void
		{
			if(responseQueue.length > 0)
			{
				responseTimer.reset();
				responseTimer.start();
			}
		}

		private function httpResponse(req:XMLNode, isPollResponse:Boolean, evt:ResultEvent):void
		{
			requestCount--;
			var rawXML:String = evt.result as String;

			if(logger)
	       		logger.log(rawXML, "INCOMING");

			var xmlData:XMLDocument = new XMLDocument();
			xmlData.ignoreWhite = this.ignoreWhite;
			xmlData.parseXML( rawXML );
			var bodyNode:XMLNode = xmlData.firstChild;

			var event:IncomingDataEvent = new IncomingDataEvent();
			event.data = xmlData;
			dispatchEvent( event );

			if(bodyNode.attributes["type"] == "terminal")
			{
				dispatchError( "BOSH Error", bodyNode.attributes["condition"], "", -1 );
				active = false;
			}

			for each(var childNode:XMLNode in bodyNode.childNodes)
			{
				if(childNode.nodeName == "stream:features")
				{
					_expireTagSearch = false;
					processConnectionResponse( bodyNode );
				}
				else
					responseQueue.push(childNode);
			}

			resetResponseProcessor();

			//if we just processed a poll response, we're no longer in mid-poll
			if(isPollResponse)
				isCurrentlyPolling = false;

			//if we have queued requests we want to send them before attempting to poll again
			//otherwise, if we don't already have a countdown going for the next poll, start one
	        if (!sendQueuedRequests() && (!pollTimer || !pollTimer.running))
	        	resetPollTimer();
		}

		private function httpError(req:XMLNode, isPollResponse:Boolean, evt:FaultEvent):void
		{
			active = false;
			dispatchError("Unknown HTTP Error", evt.fault.rootCause.text, "", -1);
		}

		protected override function sendXML(body:*):void
		{
			sendQueuedRequests(body);
		}

		private function sendQueuedRequests(body:*=null):Boolean
		{
			if(body)
				requestQueue.push(body);
			else if(requestQueue.length == 0)
				return false;

			return sendRequests();
		}

		//returns true if any requests were sent
		private function sendRequests(data:XMLNode = null, isPoll:Boolean=false):Boolean
		{
			if(requestCount >= maxConcurrentRequests)
				return false;

			if(isPoll)
				trace("Polling at: " + new Date().getSeconds());

			requestCount++;

			if(!data)
			{
				if(isPoll)
				{
					data = createRequest();
				}
				else
				{
					var temp:Array = [];
					for(var i:uint = 0; i < 10 && requestQueue.length > 0; i++)
					{
						temp.push(requestQueue.shift());
					}
					data = createRequest(temp);
				}
			}

			//build the http request
	        var request:HTTPService = new HTTPService();
	        request.method = "post";
	        request.headers = headers[request.method];
	        request.url = httpServer;
	        request.resultFormat = HTTPService.RESULT_FORMAT_TEXT;
	        request.contentType = "text/xml";

	        var responseCallback:Callback = new Callback(this, httpResponse, data, isPoll);
	        var errorCallback:Callback = new Callback(this, httpError, data, isPoll);

	        request.addEventListener(ResultEvent.RESULT, responseCallback.call, false);
	        request.addEventListener(FaultEvent.FAULT, errorCallback.call, false);

	       	if(logger)
	       		logger.log(data, "OUTGOING");

			request.send(data);

			resetPollTimer();

			return true;
		}

    	private function resetPollTimer():void
    	{
    		if(!pollTimer)
    			return;
    		pollTimer.reset();
    		pollTimer.start();
    	}

    	private function pollServer(evt:TimerEvent):void
    	{
    		//We shouldn't poll if the connection is dead, if we had requests to send instead, or if there's already one in progress
    		if(!isActive() || sendQueuedRequests() || isCurrentlyPolling)
    			return;

    		//this should be safe since sendRequests checks to be sure it's not over the concurrent requests limit, and we just ensured that the queue is empty by calling sendQueuedRequests()
    		isCurrentlyPolling = sendRequests(null, true);
    	}

    	private function get nextRID():Number {
    		if(!rid)
    			rid = Math.floor(Math.random() * 1000000);
    		return ++rid;
    	}

    	private function createRequest(bodyContent:Array=null):XMLNode {
    		var attrs:Object = {
    			xmlns: "http://jabber.org/protocol/httpbind",
				rid: nextRID,
				sid: sid
    		}
    		var req:XMLNode = new XMLNode(1, "body");
    		if(bodyContent)
    			for each(var content:XMLNode in bodyContent)
    					req.appendChild(content);

    		req.attributes = attrs;

    		return req;
    	}

	    private function handleAuthentication(responseBody:XMLNode):void
	    {
	    //    if(!response || response.length == 0) {
	     //       return;
	     //   }
	        var status:Object = auth.handleResponse(0, responseBody);
	        if (status.authComplete) {
	            if (status.authSuccess) {
	                bindConnection();
	            }
	            else {
	                dispatchEvent(new ErrorEvent(ErrorEvent.ERROR));
	                disconnect();
	            }
	        }
	    }

	    private function configureConnection(responseBody:XMLNode):void
	    {
	    	var features:XMLNode = responseBody.firstChild;

	        var authentication:Object = {};
	        for each(var feature:XMLNode in features.childNodes)
			{
	            if (feature.nodeName == "mechanisms")
	                authentication.auth = configureAuthMechanisms(feature);
	            else if (feature.nodeName == "bind")
	                authentication.bind = true;
	            else if (feature.nodeName == "session")
	                authentication.session = true;
	        }

	        auth = authentication.auth;
	        dispatchEvent(new ConnectionSuccessEvent());
	        authHandler = handleAuthentication;
	        sendXML(auth.request);
	    }

	    private function configureAuthMechanisms(mechanisms:XMLNode):SASLAuth
	    {
	        var authMechanism:SASLAuth;
	        var authClass:Class;
	        for each(var mechanism:XMLNode in mechanisms.childNodes)
	        {
	        	authClass = saslMechanisms[mechanism.firstChild.nodeValue];
	   			if(authClass)
	   				break;
	        }

	        if (!authClass) {
	        	dispatchError("SASL missing", "The server is not configured to support any available SASL mechanisms", "SASL", -1);
	        	return null;
	        }

	   		return new authClass(this);
	    }

	    public function handleBindResponse(packet:IQ):void
	    {
	    	var bind:BindExtension = packet.getExtension("bind") as BindExtension;

            var jid:String = bind.getJID();

            var parts:Array = jid.split("/");
            myResource = parts[1];
            parts = parts[0].split("@");
            myUsername = parts[0];
            domain = parts[1];

            establishSession();
	    }

	    private function bindConnection():void
	    {
	    	var bindIQ:IQ = new IQ(null, "set");

			var bindExt:BindExtension = new BindExtension();
			if(resource)
	        	bindExt.resource = resource;

	        bindIQ.addExtension(bindExt);

			//this is laaaaaame, it should be a function
			bindIQ.callbackName = "handleBindResponse";
			bindIQ.callbackScope = this;

	        send(bindIQ);
	    }

	    public function handleSessionResponse(packet:IQ):void
	    {
	    	dispatchEvent(new LoginEvent());
	    }

	    private function establishSession():void
	    {
	        var sessionIQ:IQ = new IQ(null, "set");

	        sessionIQ.addExtension(new SessionExtension());

	        sessionIQ.callbackName = "handleSessionResponse";
	        sessionIQ.callbackScope = this;

	        send(sessionIQ);
	    }

		//do nothing, we use polling instead
		public override function sendKeepAlive():void {}
	}
}

Add a little padding on BOSH polling; sacrifices a tiny bit of responsiveness for some safety, HOPEFULLY fixing XIFF-31. r=Daniel


package aerys.minko.render.geometry.stream.iterator
{
	import aerys.minko.ns.minko_stream;
	import aerys.minko.render.geometry.stream.IVertexStream;
	import aerys.minko.render.geometry.stream.IndexStream;

	import flash.utils.Proxy;
	import flash.utils.flash_proxy;

	/**
	 * TriangleIterator allow per-triangle access on VertexStream objects.
	 *
	 * @author Jean-Marc Le Roux
	 *
	 */
	public final class TriangleIterator extends Proxy
	{
		use namespace minko_stream;

		private var _singleReference	: Boolean			= true;

		private var _offset				: int				= 0;
		private var _index				: int				= 0;

		private var _vb					: IVertexStream		= null;
		private var _ib					: IndexStream		= null;

		private var _triangle			: TriangleReference	= null;

		public function get length() : int
		{
			return _ib ? _ib.length / 3 : _vb.numVertices / 3;
		}

		public function TriangleIterator(vertexStream 		: IVertexStream,
										   indexStream		: IndexStream,
										   singleReference	: Boolean = true)
		{
			super();

			_vb = vertexStream;
			_ib = indexStream;
			_singleReference = singleReference;
		}

		override flash_proxy function hasProperty(name : *) : Boolean
		{
			return int(name) < _ib.length / 3;
		}

		override flash_proxy function nextNameIndex(index : int) : int
		{
			index -= _offset;
			_offset = 0;

			return index < _ib.length / 3 ? index + 1 : 0;
		}

		override flash_proxy function nextName(index : int) : String
		{
			return String(index - 1);
		}

		override flash_proxy function nextValue(index : int) : *
		{
			_index = index - 1;

			if (!_singleReference || !_triangle)
				_triangle = new TriangleReference(_vb, _ib, _index);

			if (_singleReference)
			{
				_triangle._index = _index;
				_triangle.v0._index = _ib._data[int(_index * 3)];
				_triangle.v1._index = _ib._data[int(_index * 3 + 1)];
				_triangle.v2._index = _ib._data[int(_index * 3 + 2)];
				_triangle._update = TriangleReference.UPDATE_ALL;
			}

			return _triangle;
		}

		override flash_proxy function getProperty(name : *) : *
		{
			return new TriangleReference(_vb, _ib, int(name));
		}

		override flash_proxy function deleteProperty(name : *) : Boolean
		{
			var index 	: uint 	= uint(name);

			_ib.deleteTriangleByIndex(index);

			return true;
		}
	}
}

package aerys.minko.render.geometry.stream.iterator
{
	import aerys.minko.ns.minko_stream;
	import aerys.minko.render.geometry.stream.IVertexStream;
	import aerys.minko.render.geometry.stream.IndexStream;

	import flash.utils.Proxy;
	import flash.utils.flash_proxy;

	/**
	 * TriangleIterator allow per-triangle access on VertexStream objects.
	 *
	 * @author Jean-Marc Le Roux
	 *
	 */
	public final class TriangleIterator extends Proxy
	{
		use namespace minko_stream;

		private var _singleReference	: Boolean			= true;

		private var _offset				: int				= 0;
		private var _index				: int				= 0;

		private var _vb					: IVertexStream		= null;
		private var _ib					: IndexStream		= null;

		private var _triangle			: TriangleReference	= null;

		public function get length() : int
		{
			return _ib ? _ib.length / 3 : _vb.numVertices / 3;
		}

		public function TriangleIterator(vertexStream 		: IVertexStream,
										   indexStream		: IndexStream,
										   singleReference	: Boolean = true)
		{
			super();

			_vb = vertexStream;
			_ib = indexStream;
			_singleReference = singleReference;
		}

		override flash_proxy function hasProperty(name : *) : Boolean
		{
			return int(name) < _ib.length / 3;
		}

		override flash_proxy function nextNameIndex(index : int) : int
		{
			index -= _offset;
			_offset = 0;

			return index < _ib.length / 3 ? index + 1 : 0;
		}

		override flash_proxy function nextName(index : int) : String
		{
			return String(index - 1);
		}

		override flash_proxy function nextValue(index : int) : *
		{
			_index = index - 1;

			if (!_singleReference || !_triangle)
				_triangle = new TriangleReference(_vb, _ib, _index);

			if (_singleReference)
			{
				_triangle._index = _index;
				_triangle.v0._index = _ib._data[int(_index * 3)];
				_triangle.v1._index = _ib._data[int(_index * 3 + 1)];
				_triangle.v2._index = _ib._data[int(_index * 3 + 2)];
				_triangle._update = TriangleReference.UPDATE_ALL;
			}

			return _triangle;
		}

		override flash_proxy function getProperty(name : *) : *
		{
			return new TriangleReference(_vb, _ib, int(name));
		}

		override flash_proxy function deleteProperty(name : *) : Boolean
		{
			var index 	: uint 	= uint(name);

			_ib.deleteTriangle(index);

			return true;
		}
	}
}

fix TriangleIterator to use IVertexStream.deleteTriangle()