////////////////////////////////////////////////////////////////////////////////
// Copyright (c) 2008-2013 Esri. 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 com.esri.builder.supportClasses
{

import flash.filesystem.File;

import mx.utils.StringUtil;

public class FileUtil
{
    public static function generateUniqueRelativePath(baseDirectory:File, relativeFilePath:String, relativePathBlackList:Array = null):String
    {
        var currentID:int = 1;
        var extensionIndex:int = relativeFilePath.lastIndexOf(".");
        var extension:String = "";
        if (extensionIndex > -1)
        {
            extension = relativeFilePath.substr(extensionIndex);
            relativeFilePath = relativeFilePath.substr(0, extensionIndex);
        }
        var filenameTemplate:String = relativeFilePath + "_{0}" + extension;
        var uniqueWidgetConfigPath:String;

        do
        {
            uniqueWidgetConfigPath = StringUtil.substitute(filenameTemplate, currentID++);
        } while (doesFileExist(baseDirectory, uniqueWidgetConfigPath, relativePathBlackList));

        return uniqueWidgetConfigPath;
    }

    private static function doesFileExist(baseDirectory:File, relativeFilePath:String, relativePathBlackList:Array):Boolean
    {
        if (relativePathBlackList
            && relativePathBlackList.indexOf(relativeFilePath) > -1)
        {
            return true;
        }

        var fileToCheck:File = baseDirectory.resolvePath(relativeFilePath);
        return fileToCheck.exists;
    }

    public static function getFileName(file:File):String
    {
        return file.name.replace("." + file.extension, "");
    }

    public static function findMatchingFiles(directory:File, pattern:RegExp):Array
    {
        var moduleFiles:Array = [];

        if (directory.isDirectory)
        {
            const files:Array = directory.getDirectoryListing();

            for each (var file:File in files)
            {
                if (file.isDirectory)
                {
                    moduleFiles = moduleFiles.concat(findMatchingFiles(file, pattern));
                }
                else if (pattern.test(file.name))
                {
                    moduleFiles.push(file);
                }
            }
        }

        return moduleFiles;
    }

    public static function findMatchingFile(directory:File, pattern:RegExp):File
    {
        var matchingFile:File;

        if (directory.isDirectory)
        {
            const files:Array = directory.getDirectoryListing();

            //sort files first to reduce unnecessary directory listing calls.
            files.sortOn("isDirectory");

            for each (var file:File in files)
            {
                if (file.isDirectory)
                {
                    matchingFile = findMatchingFile(file, pattern);
                }
                else if (pattern.test(file.name))
                {
                    matchingFile = file;
                }

                if (matchingFile)
                {
                    break;
                }
            }
        }

        return matchingFile;
    }
}
}

////////////////////////////////////////////////////////////////////////////////
// Copyright (c) 2008-2013 Esri. 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 com.esri.builder.supportClasses
{

import flash.filesystem.File;

public class FileUtil
{
    public static function generateUniqueRelativePath(baseDirectory:File, relativeFilePath:String, relativePathBlackList:Array = null):String
    {
        var extensionIndex:int = relativeFilePath.lastIndexOf(".");
        var extension:String = "";
        if (extensionIndex > -1)
        {
            extension = relativeFilePath.substr(extensionIndex);
            relativeFilePath = relativeFilePath.substr(0, extensionIndex);
        }
        var filenameTemplate:String = relativeFilePath + "_{0}" + extension;

        var uniqueWidgetConfigPath:String =
            LabelUtil.generateUniqueLabel(filenameTemplate,
                                          relativePathBlackList,
                                          function isNonExistentFile(uniqueFilePath:String, availableNames:Array):Boolean
                                          {
                                              if (availableNames
                                                  && availableNames.indexOf(uniqueFilePath) > -1)
                                              {
                                                  return false;
                                              }

                                              return !baseDirectory.resolvePath(uniqueFilePath).exists;
                                          });
        return uniqueWidgetConfigPath;
    }

    public static function getFileName(file:File):String
    {
        return file.name.replace("." + file.extension, "");
    }

    public static function findMatchingFiles(directory:File, pattern:RegExp):Array
    {
        var moduleFiles:Array = [];

        if (directory.isDirectory)
        {
            const files:Array = directory.getDirectoryListing();

            for each (var file:File in files)
            {
                if (file.isDirectory)
                {
                    moduleFiles = moduleFiles.concat(findMatchingFiles(file, pattern));
                }
                else if (pattern.test(file.name))
                {
                    moduleFiles.push(file);
                }
            }
        }

        return moduleFiles;
    }

    public static function findMatchingFile(directory:File, pattern:RegExp):File
    {
        var matchingFile:File;

        if (directory.isDirectory)
        {
            const files:Array = directory.getDirectoryListing();

            //sort files first to reduce unnecessary directory listing calls.
            files.sortOn("isDirectory");

            for each (var file:File in files)
            {
                if (file.isDirectory)
                {
                    matchingFile = findMatchingFile(file, pattern);
                }
                else if (pattern.test(file.name))
                {
                    matchingFile = file;
                }

                if (matchingFile)
                {
                    break;
                }
            }
        }

        return matchingFile;
    }
}
}

Use unique label utility in FileUtil.

Use unique label utility in FileUtil.


//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2007 Three Rings Design, Inc., All Rights Reserved
// http://www.threerings.net/code/nenya/
//
// 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.flex {

import flash.display.DisplayObject;
import flash.display.DisplayObjectContainer;

import flash.events.Event;
import flash.events.IEventDispatcher;

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

import mx.controls.Menu;
import mx.controls.listClasses.BaseListData;
import mx.controls.listClasses.IListItemRenderer;
import mx.controls.menuClasses.IMenuItemRenderer;
import mx.controls.menuClasses.MenuListData;

import mx.core.mx_internal;
import mx.core.Application;
import mx.core.ClassFactory;
import mx.core.IFlexDisplayObject;
import mx.core.ScrollPolicy;

import mx.events.MenuEvent;

import mx.managers.PopUpManager;

import com.threerings.util.CommandEvent;

import com.threerings.flex.PopUpUtil;

import com.threerings.flex.menuClasses.CommandMenuItemRenderer;
import com.threerings.flex.menuClasses.CommandListData;

use namespace mx_internal;

/**
 * A pretty standard menu that can submit CommandEvents if menu items have "command" and possibly
 * "arg" properties. Commands are submitted to controllers for processing. Alternatively, you may
 * specify "callback" properties that specify a function closure to call, with the "arg" property
 * containing either a single arg or an array of args.
 *
 * Another property now supported is "iconObject", which is an already-instantiated
 * IFlexDisplayObject to use as the icon. This will only be applied if the "icon" property
 * (which specifies a Class) is not used.
 *
 * Example dataProvider array:
 * [ { label: "Go home", icon: homeIconClass,
 *     command: Controller.GO_HOME, arg: homeId },
 *   { type: "separator"},
     { label: "Crazytown", callback: setCrazy, arg: [ true, false ] },
 *   { label: "Other places", children: subMenuArray }
 * ];
 *
 * See "Defining menu structure and data" in the Flex manual for the full list.
 *
 * CommandMenu is scrollable.  This can be forced by setting verticalScrollPolicy to
 * ScrollPolicy.AUTO or ScrollPolicy.ON and setting the maxHeight.  Also, if the scrolling isn't
 * forced on, but the menu does not fit within the vertical bounds given (either the stage size by
 * default, or the height of the rectangle given in setBounds()), scrolling will be turned on
 * so that none of the content is lost.
 *
 * Note: we don't extend flexlib's ScrollableMenu (or its sub-class ScrollableArrowMenu) because it
 * does some weird stuff in measure() that forces some of our menus down to a very small height...
 */
public class CommandMenu extends Menu
{
    /**
     * Factory method to create a command menu.
     *
     * @param items an array of menu items.
     * @param dispatcher an override event dispatcher to use for command events, rather than
     * our parent.
     */
    public static function createMenu (
        items :Array, dispatcher :IEventDispatcher = null) :CommandMenu
    {
        var menu :CommandMenu = new CommandMenu();
        menu.owner = DisplayObjectContainer(Application.application);
        menu.tabEnabled = false;
        menu.showRoot = true;
        menu.setDispatcher(dispatcher);
        Menu.popUpMenu(menu, null, items); // does not actually pop up, but needed.
        return menu;
    }

    /**
     * Add a separator to the specified menu, unless it would not make sense to do so, because
     * the menu is empty or already ends with a separator.
     */
    public static function addSeparator (menuItems :Array) :void
    {
        const len :int = menuItems.length;
        if (len > 0 && menuItems[len - 1].type != "separator") {
            menuItems.push({ type: "separator" });
        }
    }

    /**
     * The mx.controls.Menu class overrides setting and getting the verticalScrollPolicy
     * basically setting the verticalScrollPolicy did nothing, and getting it always returned
     * ScrollPolicy.OFF.  So that's not going to work if we want the menu to scroll. Here we
     * reinstate the verticalScrollPolicy setter, and keep a local copy of the value in a
     * protected variable _verticalScrollPolicy.
     *
     * This setter is basically a copy of what ScrollControlBase and ListBase do.
     */
    override public function set verticalScrollPolicy (value :String) :void
    {
        var newPolicy :String = value.toLowerCase();

        itemsSizeChanged = true;

        if (_verticalScrollPolicy != newPolicy)
        {
            _verticalScrollPolicy = newPolicy;
            dispatchEvent(new Event("verticalScrollPolicyChanged"));
        }

        invalidateDisplayList();
    }

    override public function get verticalScrollPolicy () :String
    {
        return _verticalScrollPolicy;
    }

    public function CommandMenu ()
    {
        super();

        itemRenderer = new ClassFactory(getItemRenderer());

        verticalScrollPolicy = ScrollPolicy.OFF;
        addEventListener(MenuEvent.ITEM_CLICK, itemClicked);
    }

    /**
     * Called in the CommandMenu constructor, this should return the item rendering class for this
     * CommandMenu.
     */
    protected function getItemRenderer () :Class
    {
        return CommandMenuItemRenderer;
    }

    /**
     * Configures the event dispatcher to be used when dispatching this menu's command events.
     * Normally they will be dispatched on our parent (usually the SystemManager or something).
     */
    public function setDispatcher (dispatcher :IEventDispatcher) :void
    {
        _dispatcher = dispatcher;
    }

    /**
     * Sets a Rectangle (in stage coords) that the menu will attempt to keep submenus positioned
     * within.  If a submenu is too large to fit, it will position it in the lower right corner
     * of this Rectangle (or top if popping upwards, or left if popping leftwards).   This
     * value defaults to the stage bounds.
     */
    public function setBounds (fitting :Rectangle) :void
    {
        _fitting = fitting;
    }

    /**
     * Actually pop up the menu. This can be used instead of show().
     */
    public function popUp (
        trigger :DisplayObject, popUpwards :Boolean = false, popLeftwards :Boolean = false) :void
    {
        _upping = popUpwards;
        _lefting = popLeftwards;
        var r :Rectangle = trigger.getBounds(trigger.stage);
        show(_lefting ? r.left : r.right, _upping ? r.top : r.bottom);
    }

    /**
     * Shows the menu at the specified mouse coordinates.
     */
    public function popUpAt (
        mx :int, my :int, popUpwards :Boolean = false, popLeftwards :Boolean = false) :void
    {
        _upping = popUpwards;
        _lefting = popLeftwards;
        show(mx, my);
    }

    /**
     * Shows the menu at the current mouse location.
     */
    public function popUpAtMouse (popUpwards :Boolean = false, popLeftwards :Boolean = false) :void
    {
        _upping = popUpwards;
        _lefting = popLeftwards;
        show(); // our show, with no args, pops at the mouse
    }

    /**
     * Just like our superclass's show(), except that when invoked with no args, causes the menu to
     * show at the current mouse location instead of the top-left corner of the application.
     * Also, we ensure that the resulting menu is in-bounds.
     */
    override public function show (xShow :Object = null, yShow :Object = null) :void
    {
        if (xShow == null) {
            xShow = DisplayObject(Application.application).mouseX;
        }
        if (yShow == null) {
            yShow = DisplayObject(Application.application).mouseY;
        }

        super.show(xShow, yShow);

        // reposition now that we know our size
        if (_lefting) {
            y = x - getExplicitOrMeasuredWidth();
        }
        if (_upping) {
            y = y - getExplicitOrMeasuredHeight();
        }

        var fitting :Rectangle = _fitting || screen;
        PopUpUtil.fitInRect(this, fitting);

        // if after fitting as best we can, the menu is outside of the declared bounds, we force
        // scrolling and set the max height
        if (y < fitting.y) {
            verticalScrollPolicy = ScrollPolicy.AUTO;
            maxHeight = fitting.height;
            y = fitting.y;
        }
    }

    /**
     * The Menu class overrode configureScrollBars() and made the function do nothing.  That means
     * the scrollbars don't know how to draw themselves, so here we reinstate configureScrollBars.
     * This is basically a copy of the same method from the mx.controls.List class.
     */
    override protected function configureScrollBars () :void
    {
        var rowCount :int = listItems.length;
        if (rowCount == 0) {
            return;
        }

        // if there is more than one row and it is a partial row we don't count it
        if (rowCount > 1 &&
            rowInfo[rowCount - 1].y + rowInfo[rowCount - 1].height > listContent.height) {
            rowCount--;
        }

        // offset, when added to rowCount, is hte index of the dataProvider item for that row.
        // IOW, row 10 in listItems is showing dataProvider item 10 + verticalScrollPosition -
        // lockedRowCount - 1
        var offset :int = verticalScrollPosition - lockedRowCount - 1;

        // don't count filler rows at the bottom either.
        var fillerRows :int = 0;
        while (rowCount > 0 && listItems[rowCount - 1].length == 0)
        {
            if (collection && rowCount + offset >= collection.length) {
                rowCount--;
                fillerRows++;
            } else {
                break;
            }
        }

        var colCount :int = listItems[0].length;
        var oldHorizontalScrollBar :Object = horizontalScrollBar;
        var oldVerticalScrollBar :Object = verticalScrollBar;
        var roundedWidth :int = Math.round(unscaledWidth);
        var length :int = collection ? collection.length - lockedRowCount : 0;
        var numRows :int = rowCount - lockedRowCount;

        setScrollBarProperties(Math.round(listContent.width), roundedWidth, length, numRows);
        maxVerticalScrollPosition = Math.max(length - numRows, 0);
    }

    /**
     * Callback for MenuEvent.ITEM_CLICK.
     */
    protected function itemClicked (event :MenuEvent) :void
    {
        var arg :Object = getItemProp(event.item, "arg");
        var cmdOrFn :Object = getItemProp(event.item, "command");
        if (cmdOrFn == null) {
            cmdOrFn = getItemProp(event.item, "callback");
        }
        if (cmdOrFn != null) {
            event.stopImmediatePropagation();
            CommandEvent.dispatch(_dispatcher == null ? mx_internal::parentDisplayObject :
                                  _dispatcher, cmdOrFn, arg);
        }
        // else: no warning. There may be non-command menu items mixed in.
    }

    // from ScrollableArrowMenu..
    override mx_internal function openSubMenu (row :IListItemRenderer) :void
    {
        supposedToLoseFocus = true;

        var r :Menu = getRootMenu();
        var menu :CommandMenu;

        // check to see if the menu exists, if not create it
        if (!IMenuItemRenderer(row).menu) {
            // the only differences between this method and the original method in mx.controls.Menu
            // are these few lines.
            menu = new CommandMenu();
            menu.maxHeight = this.maxHeight;
            menu.verticalScrollPolicy = this.verticalScrollPolicy;
            menu.variableRowHeight = this.variableRowHeight;

            menu.parentMenu = this;
            menu.owner = this;
            menu.showRoot = showRoot;
            menu.dataDescriptor = r.dataDescriptor;
            menu.styleName = r;
            menu.labelField = r.labelField;
            menu.labelFunction = r.labelFunction;
            menu.iconField = r.iconField;
            menu.iconFunction = r.iconFunction;
            menu.itemRenderer = r.itemRenderer;
            menu.rowHeight = r.rowHeight;
            menu.scaleY = r.scaleY;
            menu.scaleX = r.scaleX;

            // if there's data and it has children then add the items
            if (row.data && _dataDescriptor.isBranch(row.data) &&
                    _dataDescriptor.hasChildren(row.data)) {
                menu.dataProvider = _dataDescriptor.getChildren(row.data);
            }
            menu.sourceMenuBar = sourceMenuBar;
            menu.sourceMenuBarItem = sourceMenuBarItem;

            IMenuItemRenderer(row).menu = menu;
            PopUpManager.addPopUp(menu, r, false);
        }

        super.openSubMenu(row);

        // if we're lefting, upping or fitting make sure our submenu does so as well
        var submenu :Menu = IMenuItemRenderer(row).menu;
        if (_lefting) {
            submenu.x -= submenu.getExplicitOrMeasuredWidth();
        }
        if (_upping) {
            var displayObj :DisplayObject = DisplayObject(row);
            var rowLoc :Point = displayObj.localToGlobal(new Point(row.x, row.y));
            submenu.y = rowLoc.y - submenu.getExplicitOrMeasuredHeight() + displayObj.height;
        }

        var fitting :Rectangle = _fitting || screen;
        PopUpUtil.fitInRect(submenu, fitting);
        // if after fitting as best we can, the menu is outside of the declared bounds, we force
        // scrolling and set the max height
        if (submenu.y < fitting.y) {
            submenu.verticalScrollPolicy = ScrollPolicy.AUTO;
            submenu.maxHeight = fitting.height;
            submenu.y = fitting.y;
        }
    }

    override protected function measure () :void
    {
        super.measure();

        if (measuredHeight > this.maxHeight) {
            measuredHeight = this.maxHeight;
        }

        if (verticalScrollPolicy == ScrollPolicy.ON || verticalScrollPolicy == ScrollPolicy.AUTO) {
            if (verticalScrollBar) {
                measuredMinWidth = measuredWidth = measuredWidth + verticalScrollBar.minWidth;
            }
        }

        commitProperties();
    }

    // from List
    override protected function makeListData (data :Object, uid :String, rowNum :int) :BaseListData
    {
        // Oh, FFS.
        // We need to set up these "maxMeasuredIconWidth" fields on the MenuListData, but our
        // superclass has made those variables private.
        // We can get the values out of another MenuListData, so we just always call super()
        // to create one of those, and if we need to make a CommandListData, we construct one
        // from the fields in the MenuListData.

        var menuListData :MenuListData = super.makeListData(data, uid, rowNum) as MenuListData;

        var iconObject :IFlexDisplayObject = getItemProp(data, "iconObject") as IFlexDisplayObject;
        if (iconObject != null) {
            var cmdListData :CommandListData = new CommandListData(menuListData.label,
                menuListData.icon, iconObject, labelField, uid, this, rowNum);

            cmdListData.maxMeasuredIconWidth = menuListData.maxMeasuredIconWidth;
            cmdListData.maxMeasuredTypeIconWidth = menuListData.maxMeasuredTypeIconWidth;
            cmdListData.maxMeasuredBranchIconWidth = menuListData.maxMeasuredBranchIconWidth;
            cmdListData.useTwoColumns = menuListData.useTwoColumns;
            return cmdListData;
        }

        return menuListData;
    }

    /**
     * Get the specified property for the specified item, if any.  Somewhat similar to bits in the
     * DefaultDataDescriptor.
     */
    protected function getItemProp (item :Object, prop :String) :Object
    {
        try {
            if (item is XML) {
                return String((item as XML).attribute(prop));

            } else if (prop in item) {
                return item[prop];
            }

        } catch (e :Error) {
            // alas; fall through
        }
        return null;
    }

    protected var _dispatcher :IEventDispatcher;
    protected var _lefting :Boolean = false;
    protected var _upping :Boolean = false;
    protected var _fitting :Rectangle = null;
    protected var _verticalScrollPolicy :String;
}
}

//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2007 Three Rings Design, Inc., All Rights Reserved
// http://www.threerings.net/code/nenya/
//
// 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.flex {

import flash.display.DisplayObject;
import flash.display.DisplayObjectContainer;

import flash.events.Event;
import flash.events.IEventDispatcher;

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

import mx.controls.Menu;
import mx.controls.listClasses.BaseListData;
import mx.controls.listClasses.IListItemRenderer;
import mx.controls.menuClasses.IMenuItemRenderer;
import mx.controls.menuClasses.MenuListData;

import mx.core.mx_internal;
import mx.core.Application;
import mx.core.ClassFactory;
import mx.core.IFlexDisplayObject;
import mx.core.ScrollPolicy;

import mx.events.MenuEvent;

import mx.managers.PopUpManager;

import com.threerings.util.CommandEvent;

import com.threerings.flex.PopUpUtil;

import com.threerings.flex.menuClasses.CommandMenuItemRenderer;
import com.threerings.flex.menuClasses.CommandListData;

use namespace mx_internal;

/**
 * A pretty standard menu that can submit CommandEvents if menu items have "command" and possibly
 * "arg" properties. Commands are submitted to controllers for processing. Alternatively, you may
 * specify "callback" properties that specify a function closure to call, with the "arg" property
 * containing either a single arg or an array of args.
 *
 * Another property now supported is "iconObject", which is an already-instantiated
 * IFlexDisplayObject to use as the icon. This will only be applied if the "icon" property
 * (which specifies a Class) is not used.
 *
 * Example dataProvider array:
 * [ { label: "Go home", icon: homeIconClass,
 *     command: Controller.GO_HOME, arg: homeId },
 *   { type: "separator"},
     { label: "Crazytown", callback: setCrazy, arg: [ true, false ] },
 *   { label: "Other places", children: subMenuArray }
 * ];
 *
 * See "Defining menu structure and data" in the Flex manual for the full list.
 *
 * CommandMenu is scrollable.  This can be forced by setting verticalScrollPolicy to
 * ScrollPolicy.AUTO or ScrollPolicy.ON and setting the maxHeight.  Also, if the scrolling isn't
 * forced on, but the menu does not fit within the vertical bounds given (either the stage size by
 * default, or the height of the rectangle given in setBounds()), scrolling will be turned on
 * so that none of the content is lost.
 *
 * Note: we don't extend flexlib's ScrollableMenu (or its sub-class ScrollableArrowMenu) because it
 * does some weird stuff in measure() that forces some of our menus down to a very small height...
 */
public class CommandMenu extends Menu
{
    /**
     * Factory method to create a command menu.
     *
     * @param items an array of menu items.
     * @param dispatcher an override event dispatcher to use for command events, rather than
     * our parent.
     */
    public static function createMenu (
        items :Array, dispatcher :IEventDispatcher = null) :CommandMenu
    {
        var menu :CommandMenu = new CommandMenu();
        menu.owner = DisplayObjectContainer(Application.application);
        menu.tabEnabled = false;
        menu.showRoot = true;
        menu.setDispatcher(dispatcher);
        Menu.popUpMenu(menu, null, items); // does not actually pop up, but needed.
        return menu;
    }

    /**
     * Add a separator to the specified menu, unless it would not make sense to do so, because
     * the menu is empty or already ends with a separator.
     */
    public static function addSeparator (menuItems :Array) :void
    {
        const len :int = menuItems.length;
        if (len > 0 && menuItems[len - 1].type != "separator") {
            menuItems.push({ type: "separator" });
        }
    }

    /**
     * The mx.controls.Menu class overrides setting and getting the verticalScrollPolicy
     * basically setting the verticalScrollPolicy did nothing, and getting it always returned
     * ScrollPolicy.OFF.  So that's not going to work if we want the menu to scroll. Here we
     * reinstate the verticalScrollPolicy setter, and keep a local copy of the value in a
     * protected variable _verticalScrollPolicy.
     *
     * This setter is basically a copy of what ScrollControlBase and ListBase do.
     */
    override public function set verticalScrollPolicy (value :String) :void
    {
        var newPolicy :String = value.toLowerCase();

        itemsSizeChanged = true;

        if (_verticalScrollPolicy != newPolicy)
        {
            _verticalScrollPolicy = newPolicy;
            dispatchEvent(new Event("verticalScrollPolicyChanged"));
        }

        invalidateDisplayList();
    }

    override public function get verticalScrollPolicy () :String
    {
        return _verticalScrollPolicy;
    }

    public function CommandMenu ()
    {
        super();

        itemRenderer = new ClassFactory(getItemRenderer());

        verticalScrollPolicy = ScrollPolicy.OFF;
        addEventListener(MenuEvent.ITEM_CLICK, itemClicked);
    }

    /**
     * Called in the CommandMenu constructor, this should return the item rendering class for this
     * CommandMenu.
     */
    protected function getItemRenderer () :Class
    {
        return CommandMenuItemRenderer;
    }

    /**
     * Configures the event dispatcher to be used when dispatching this menu's command events.
     * Normally they will be dispatched on our parent (usually the SystemManager or something).
     */
    public function setDispatcher (dispatcher :IEventDispatcher) :void
    {
        _dispatcher = dispatcher;
    }

    /**
     * Sets a Rectangle (in stage coords) that the menu will attempt to keep submenus positioned
     * within.  If a submenu is too large to fit, it will position it in the lower right corner
     * of this Rectangle (or top if popping upwards, or left if popping leftwards).   This
     * value defaults to the stage bounds.
     */
    public function setBounds (fitting :Rectangle) :void
    {
        _fitting = fitting;
    }

    /**
     * Actually pop up the menu. This can be used instead of show().
     */
    public function popUp (
        trigger :DisplayObject, popUpwards :Boolean = false, popLeftwards :Boolean = false) :void
    {
        _upping = popUpwards;
        _lefting = popLeftwards;
        var r :Rectangle = trigger.getBounds(trigger.stage);
        show(_lefting ? r.left : r.right, _upping ? r.top : r.bottom);
    }

    /**
     * Shows the menu at the specified mouse coordinates.
     */
    public function popUpAt (
        mx :int, my :int, popUpwards :Boolean = false, popLeftwards :Boolean = false) :void
    {
        _upping = popUpwards;
        _lefting = popLeftwards;
        show(mx, my);
    }

    /**
     * Shows the menu at the current mouse location.
     */
    public function popUpAtMouse (popUpwards :Boolean = false, popLeftwards :Boolean = false) :void
    {
        _upping = popUpwards;
        _lefting = popLeftwards;
        show(); // our show, with no args, pops at the mouse
    }

    /**
     * Just like our superclass's show(), except that when invoked with no args, causes the menu to
     * show at the current mouse location instead of the top-left corner of the application.
     * Also, we ensure that the resulting menu is in-bounds.
     */
    override public function show (xShow :Object = null, yShow :Object = null) :void
    {
        if (xShow == null) {
            xShow = DisplayObject(Application.application).mouseX;
        }
        if (yShow == null) {
            yShow = DisplayObject(Application.application).mouseY;
        }

        super.show(xShow, yShow);

        // reposition now that we know our size
        if (_lefting) {
            y = x - getExplicitOrMeasuredWidth();
        }
        if (_upping) {
            y = y - getExplicitOrMeasuredHeight();
        }

        var fitting :Rectangle = _fitting || screen;
        PopUpUtil.fitInRect(this, fitting);

        // if after fitting as best we can, the menu is outside of the declared bounds, we force
        // scrolling and set the max height
        if (y < fitting.y) {
            verticalScrollPolicy = ScrollPolicy.AUTO;
            maxHeight = fitting.height;
            y = fitting.y;
        }
    }

    /**
     * The Menu class overrode configureScrollBars() and made the function do nothing.  That means
     * the scrollbars don't know how to draw themselves, so here we reinstate configureScrollBars.
     * This is basically a copy of the same method from the mx.controls.List class.
     */
    override protected function configureScrollBars () :void
    {
        var rowCount :int = listItems.length;
        if (rowCount == 0) {
            return;
        }

        // if there is more than one row and it is a partial row we don't count it
        if (rowCount > 1 &&
            rowInfo[rowCount - 1].y + rowInfo[rowCount - 1].height > listContent.height) {
            rowCount--;
        }

        // offset, when added to rowCount, is hte index of the dataProvider item for that row.
        // IOW, row 10 in listItems is showing dataProvider item 10 + verticalScrollPosition -
        // lockedRowCount - 1
        var offset :int = verticalScrollPosition - lockedRowCount - 1;

        // don't count filler rows at the bottom either.
        var fillerRows :int = 0;
        while (rowCount > 0 && listItems[rowCount - 1].length == 0)
        {
            if (collection && rowCount + offset >= collection.length) {
                rowCount--;
                fillerRows++;
            } else {
                break;
            }
        }

        var colCount :int = listItems[0].length;
        var oldHorizontalScrollBar :Object = horizontalScrollBar;
        var oldVerticalScrollBar :Object = verticalScrollBar;
        var roundedWidth :int = Math.round(unscaledWidth);
        var length :int = collection ? collection.length - lockedRowCount : 0;
        var numRows :int = rowCount - lockedRowCount;

        setScrollBarProperties(Math.round(listContent.width), roundedWidth, length, numRows);
        maxVerticalScrollPosition = Math.max(length - numRows, 0);
    }

    /**
     * Callback for MenuEvent.ITEM_CLICK.
     */
    protected function itemClicked (event :MenuEvent) :void
    {
        var arg :Object = getItemProp(event.item, "arg");
        var cmdOrFn :Object = getItemProp(event.item, "command");
        if (cmdOrFn == null) {
            cmdOrFn = getItemProp(event.item, "callback");
        }
        if (cmdOrFn != null) {
            event.stopImmediatePropagation();
            CommandEvent.dispatch(_dispatcher == null ? mx_internal::parentDisplayObject :
                                  _dispatcher, cmdOrFn, arg);
        }
        // else: no warning. There may be non-command menu items mixed in.
    }

    // from ScrollableArrowMenu..
    override mx_internal function openSubMenu (row :IListItemRenderer) :void
    {
        supposedToLoseFocus = true;

        var r :Menu = getRootMenu();
        var menu :CommandMenu;

        // check to see if the menu exists, if not create it
        if (!IMenuItemRenderer(row).menu) {
            // the only differences between this method and the original method in mx.controls.Menu
            // are these few lines.
            menu = new CommandMenu();
            menu.maxHeight = this.maxHeight;
            menu.verticalScrollPolicy = this.verticalScrollPolicy;
            menu.variableRowHeight = this.variableRowHeight;

            menu.parentMenu = this;
            menu.owner = this;
            menu.showRoot = showRoot;
            menu.dataDescriptor = r.dataDescriptor;
            menu.styleName = r;
            menu.labelField = r.labelField;
            menu.labelFunction = r.labelFunction;
            menu.iconField = r.iconField;
            menu.iconFunction = r.iconFunction;
            menu.itemRenderer = r.itemRenderer;
            menu.rowHeight = r.rowHeight;
            menu.scaleY = r.scaleY;
            menu.scaleX = r.scaleX;

            // if there's data and it has children then add the items
            if (row.data && _dataDescriptor.isBranch(row.data) &&
                    _dataDescriptor.hasChildren(row.data)) {
                menu.dataProvider = _dataDescriptor.getChildren(row.data);
            }
            menu.sourceMenuBar = sourceMenuBar;
            menu.sourceMenuBarItem = sourceMenuBarItem;

            IMenuItemRenderer(row).menu = menu;
            PopUpManager.addPopUp(menu, r, false);
        }

        super.openSubMenu(row);

        // if we're lefting, upping or fitting make sure our submenu does so as well
        var submenu :Menu = IMenuItemRenderer(row).menu;
        if (_lefting) {
            submenu.x -= submenu.getExplicitOrMeasuredWidth();
        }
        if (_upping) {
            var displayObj :DisplayObject = DisplayObject(row);
            var rowLoc :Point = displayObj.localToGlobal(new Point());
            submenu.y = rowLoc.y - submenu.getExplicitOrMeasuredHeight() + displayObj.height;
        }

        var fitting :Rectangle = _fitting || screen;
        PopUpUtil.fitInRect(submenu, fitting);
        // if after fitting as best we can, the menu is outside of the declared bounds, we force
        // scrolling and set the max height
        if (submenu.y < fitting.y) {
            submenu.verticalScrollPolicy = ScrollPolicy.AUTO;
            submenu.maxHeight = fitting.height;
            submenu.y = fitting.y;
        }
    }

    override protected function measure () :void
    {
        super.measure();

        if (measuredHeight > this.maxHeight) {
            measuredHeight = this.maxHeight;
        }

        if (verticalScrollPolicy == ScrollPolicy.ON || verticalScrollPolicy == ScrollPolicy.AUTO) {
            if (verticalScrollBar) {
                measuredMinWidth = measuredWidth = measuredWidth + verticalScrollBar.minWidth;
            }
        }

        commitProperties();
    }

    // from List
    override protected function makeListData (data :Object, uid :String, rowNum :int) :BaseListData
    {
        // Oh, FFS.
        // We need to set up these "maxMeasuredIconWidth" fields on the MenuListData, but our
        // superclass has made those variables private.
        // We can get the values out of another MenuListData, so we just always call super()
        // to create one of those, and if we need to make a CommandListData, we construct one
        // from the fields in the MenuListData.

        var menuListData :MenuListData = super.makeListData(data, uid, rowNum) as MenuListData;

        var iconObject :IFlexDisplayObject = getItemProp(data, "iconObject") as IFlexDisplayObject;
        if (iconObject != null) {
            var cmdListData :CommandListData = new CommandListData(menuListData.label,
                menuListData.icon, iconObject, labelField, uid, this, rowNum);

            cmdListData.maxMeasuredIconWidth = menuListData.maxMeasuredIconWidth;
            cmdListData.maxMeasuredTypeIconWidth = menuListData.maxMeasuredTypeIconWidth;
            cmdListData.maxMeasuredBranchIconWidth = menuListData.maxMeasuredBranchIconWidth;
            cmdListData.useTwoColumns = menuListData.useTwoColumns;
            return cmdListData;
        }

        return menuListData;
    }

    /**
     * Get the specified property for the specified item, if any.  Somewhat similar to bits in the
     * DefaultDataDescriptor.
     */
    protected function getItemProp (item :Object, prop :String) :Object
    {
        try {
            if (item is XML) {
                return String((item as XML).attribute(prop));

            } else if (prop in item) {
                return item[prop];
            }

        } catch (e :Error) {
            // alas; fall through
        }
        return null;
    }

    protected var _dispatcher :IEventDispatcher;
    protected var _lefting :Boolean = false;
    protected var _upping :Boolean = false;
    protected var _fitting :Rectangle = null;
    protected var _verticalScrollPolicy :String;
}
}

Bugfix: fix y position of a submenu on an upwards-popping menu.
When transforming from row coords to global coords, don't use a point
in the row's parent's coords.

git-svn-id: b675b909355d5cf946977f44a8ec5a6ceb3782e4@804 ed5b42cb-e716-0410-a449-f6a68f950b19


package aerys.minko.type.math
{
	import aerys.minko.ns.minko_math;
	import aerys.minko.type.Factory;
	import aerys.minko.type.Signal;
	import aerys.minko.type.binding.IWatchable;

	import flash.geom.Vector3D;

	public class Vector4 implements IWatchable
	{
		use namespace minko_math;

		public static const X_AXIS				: Vector4	= new Vector4(1., 0., 0.);
		public static const Y_AXIS				: Vector4	= new Vector4(0., 1., 0.);
		public static const Z_AXIS				: Vector4	= new Vector4(0., 0., 1.);
		public static const ZERO				: Vector4	= new Vector4(0., 0., 0., 0.);
		public static const ONE					: Vector4	= new Vector4(1., 1., 1., 1.);
		public static const FORWARD				: Vector4	= Z_AXIS;
		public static const BACKWARD			: Vector4	= new Vector4(0., 0., -1.);
		public static const LEFT				: Vector4	= new Vector4(-1, 0., 0.);
		public static const RIGHT				: Vector4	= new Vector4(1, 0., 0.);
		public static const UP					: Vector4	= Y_AXIS;
		public static const DOWN				: Vector4	= new Vector4(0, -1., 0.);

		private static const FACTORY			: Factory	= Factory.getFactory(Vector4);

		private static const UPDATE_NONE		: uint		= 0;
		private static const UPDATE_LENGTH		: uint		= 1;
		private static const UPDATE_LENGTH_SQ	: uint		= 2;
		private static const UPDATE_ALL			: uint		= 3; // UPDATE_LENGTH | UPDATE_LENGTH_SQ;

		private static const DATA_DESCRIPTOR	: Object	= {
			'x'	: 'x',
			'y'	: 'y',
			'z'	: 'z',
			'w'	: 'w'
		}

		minko_math var _vector	: Vector3D	= new Vector3D();

		private var _update		: uint		= 0;

		private var _length		: Number	= 0.;
		private var _lengthSq	: Number	= 0.;

		private var _locked		: Boolean	= false;
		private var _changed	: Signal	= new Signal('Vector4.changed');

		public function get x()	: Number
		{
			return _vector.x;
		}
		public function set x(value : Number) : void
		{
			if (value != _vector.x)
			{
				_vector.x = value;
				_update = UPDATE_ALL;

				if (!_locked)
					_changed.execute(this);
			}
		}

		public function get y()	: Number
		{
			return _vector.y;
		}
		public function set y(value : Number) : void
		{
			if (value != _vector.y)
			{
				_vector.y = value;
				_update = UPDATE_ALL;

				if (!_locked)
					_changed.execute(this);
			}
		}

		public function get z() : Number
		{
			return _vector.z;
		}
		public function set z(value : Number) : void
		{
			if (value != _vector.z)
			{
				_vector.z = value;
				_update = UPDATE_ALL;

				if (!_locked)
					_changed.execute(this);
			}
		}

		public function get w()	: Number
		{
			return _vector.w;
		}
		public function set w(value : Number) : void
		{
			if (value != _vector.w)
			{
				_vector.w = value;
				_update = UPDATE_ALL;

				if (!_locked)
					_changed.execute(this);
			}
		}

		public function get lengthSquared()	: Number
		{
			if (_update & UPDATE_LENGTH_SQ)
			{
				_update ^= UPDATE_LENGTH_SQ;
				_lengthSq = _vector.x * _vector.x + _vector.y * _vector.y
							+ _vector.z * _vector.z;
			}

			return _lengthSq;
		}

		public function get length() : Number
		{
			if (_update & UPDATE_LENGTH)
			{
				_update ^= UPDATE_LENGTH;
				_length = Math.sqrt(lengthSquared);
			}

			return _length;
		}

		public function get locked() : Boolean
		{
			return _locked;
		}

		public function get changed() : Signal
		{
			return _changed;
		}

		public function Vector4(x 	: Number	= 0.,
								y	: Number	= 0.,
								z	: Number	= 0.,
								w 	: Number	= 1.)
		{
			_vector.x = x;
			_vector.y = y;
			_vector.z = z;
			_vector.w = w;

			_update = UPDATE_ALL;
		}

		public static function add(u 	: Vector4,
								   v 	: Vector4,
								   out	: Vector4 = null) : Vector4
		{
			out = copy(u, out);

			return out.incrementBy(v);
		}

		public static function subtract(u 	: Vector4,
										v 	: Vector4,
										out : Vector4 = null) : Vector4
		{
			out = copy(u, out);

			return out.decrementBy(v);
		}

		public static function dotProduct(u : Vector4, v : Vector4) : Number
		{
			return u._vector.dotProduct(v._vector);
		}

		public static function crossProduct(u : Vector4, v : Vector4) : Vector4
		{
			return new Vector4(
				u._vector.y * v._vector.z - v._vector.y * u._vector.z,
				u._vector.z * v._vector.x - v._vector.z * u._vector.x,
				u._vector.x * v._vector.y - v._vector.x * u._vector.y
			);
		}

		public function compareTo(v : Vector4, allFour : Boolean = false) : Boolean
		{
			return _vector.x == v._vector.x
				   && _vector.y == v._vector.y
				   && _vector.z == v._vector.z
				   && (!allFour || (_vector.w == v._vector.w));
		}

		public static function distance(u : Vector4, v : Vector4) : Number
		{
			return Vector3D.distance(u._vector, v._vector);
		}

		public function copyFrom(source : Vector4) : Vector4
		{
			return set(source.x, source.y, source.z, source.w);
		}

		public function incrementBy(vector : Vector4) : Vector4
		{
			_vector.incrementBy(vector._vector);

			_update = UPDATE_ALL;
			_changed.execute(this);

			return this;
		}

		public function decrementBy(vector : Vector4) : Vector4
		{
			_vector.decrementBy(vector._vector);

			_update = UPDATE_ALL;
			_changed.execute(this);

			return this;
		}

		public function scaleBy(scale : Number) : Vector4
		{
			_vector.scaleBy(scale);

			_update = UPDATE_ALL;
			_changed.execute(this);

			return this;
		}

		public function lerp(target : Vector4, ratio : Number) : Vector4
		{
			if (ratio == 1.)
				return copyFrom(target);

			if (ratio != 0.)
			{
				var x : Number = _vector.x;
				var y : Number = _vector.y;
				var z : Number = _vector.z;
				var w : Number = _vector.w;

				set(
					x + (target._vector.x - x) * ratio,
					y + (target._vector.y - y) * ratio,
					z + (target._vector.z - z) * ratio,
					w + (target._vector.w - w) * ratio
				);
			}

			return this;
		}

		public function equals(v : Vector4, allFour : Boolean = false, tolerance : Number = 0.) : Boolean
		{
			return tolerance
				? _vector.nearEquals(v._vector, tolerance, allFour);
				: _vector.equals(v._vector, allFour)
		}

		public function project() : Vector4
		{
			_vector.x /= _vector.w;
			_vector.y /= _vector.w;
			_vector.z /= _vector.w;

			_update = UPDATE_ALL;
			_changed.execute(this);

			return this;
		}

		public function normalize() : Vector4
		{
			var l : Number = length;

			if (l != 0.)
			{
				_vector.x /= l;
				_vector.y /= l;
				_vector.z /= l;

				_length = 1.;
				_lengthSq = 1.;

				_update = UPDATE_NONE;
				_changed.execute(this);
			}

			return this;
		}

		public function crossProduct(vector : Vector4) : Vector4
		{
			var x : Number = _vector.x;
			var y : Number = _vector.y;
			var z : Number = _vector.z;

			_vector.x = y * vector._vector.z - vector._vector.y * z;
			_vector.y = z * vector._vector.x - vector._vector.z * x;
			_vector.z = x * vector._vector.y - vector._vector.x * y;

			_update = UPDATE_ALL;
			_changed.execute(this);

			return this;
		}

		public function getVector3D() : Vector3D
		{
			return _vector.clone();
		}

		public function set(x : Number,
							y : Number,
							z : Number,
							w : Number = 1.) : Vector4
		{
			if (x != _vector.x || y != _vector.y || z != _vector.z || w != _vector.w)
			{
				_vector.setTo(x, y, z);
				_vector.w = w;

				_update = UPDATE_ALL;
				if (!_locked)
					_changed.execute(this);
			}

			return this;
		}

		public function toString() : String
		{
			return '(' + _vector.x + ', ' + _vector.y + ', ' + _vector.z + ', ' + _vector.w + ')';
		}

		public static function scale(v : Vector4, s : Number, out : Vector4 = null) : Vector4
		{
			out ||= FACTORY.create() as Vector4;
			out.set(v._vector.x * s, v._vector.y * s, v._vector.z * s, v._vector.w * s);

			return out;
		}

		public function toVector3D(out : Vector3D = null) : Vector3D
		{
			if (!out)
				return _vector.clone();

			out.x = _vector.x;
			out.y = _vector.y;
			out.z = _vector.z;
			out.w = _vector.w;

			return out;
		}

		public static function normalize(v : Vector4, out : Vector4 = null) : Vector4
		{
			out = copy(v, out);

			return out.normalize();
		}

		public function lock() : void
		{
			_locked = true;
		}

		public function unlock() : void
		{
			_locked = false;
			_changed.execute(this);
		}

		public function clone() : Vector4
		{
			return new Vector4(_vector.x, _vector.y, _vector.z, _vector.w);
		}
	}
}

package aerys.minko.type.math
{
	import aerys.minko.ns.minko_math;
	import aerys.minko.type.Factory;
	import aerys.minko.type.Signal;
	import aerys.minko.type.binding.IWatchable;

	import flash.geom.Vector3D;

	public class Vector4 implements IWatchable
	{
		use namespace minko_math;

		public static const X_AXIS				: Vector4	= new Vector4(1., 0., 0.);
		public static const Y_AXIS				: Vector4	= new Vector4(0., 1., 0.);
		public static const Z_AXIS				: Vector4	= new Vector4(0., 0., 1.);
		public static const ZERO				: Vector4	= new Vector4(0., 0., 0., 0.);
		public static const ONE					: Vector4	= new Vector4(1., 1., 1., 1.);
		public static const FORWARD				: Vector4	= Z_AXIS;
		public static const BACKWARD			: Vector4	= new Vector4(0., 0., -1.);
		public static const LEFT				: Vector4	= new Vector4(-1, 0., 0.);
		public static const RIGHT				: Vector4	= new Vector4(1, 0., 0.);
		public static const UP					: Vector4	= Y_AXIS;
		public static const DOWN				: Vector4	= new Vector4(0, -1., 0.);

		private static const FACTORY			: Factory	= Factory.getFactory(Vector4);

		private static const UPDATE_NONE		: uint		= 0;
		private static const UPDATE_LENGTH		: uint		= 1;
		private static const UPDATE_LENGTH_SQ	: uint		= 2;
		private static const UPDATE_ALL			: uint		= 3; // UPDATE_LENGTH | UPDATE_LENGTH_SQ;

		private static const DATA_DESCRIPTOR	: Object	= {
			'x'	: 'x',
			'y'	: 'y',
			'z'	: 'z',
			'w'	: 'w'
		};

		minko_math var _vector	: Vector3D	= new Vector3D();

		private var _update		: uint		= 0;

		private var _length		: Number	= 0.;
		private var _lengthSq	: Number	= 0.;

		private var _locked		: Boolean	= false;
		private var _changed	: Signal	= new Signal('Vector4.changed');

		public function get x()	: Number
		{
			return _vector.x;
		}
		public function set x(value : Number) : void
		{
			if (value != _vector.x)
			{
				_vector.x = value;
				_update = UPDATE_ALL;

				if (!_locked)
					_changed.execute(this);
			}
		}

		public function get y()	: Number
		{
			return _vector.y;
		}
		public function set y(value : Number) : void
		{
			if (value != _vector.y)
			{
				_vector.y = value;
				_update = UPDATE_ALL;

				if (!_locked)
					_changed.execute(this);
			}
		}

		public function get z() : Number
		{
			return _vector.z;
		}
		public function set z(value : Number) : void
		{
			if (value != _vector.z)
			{
				_vector.z = value;
				_update = UPDATE_ALL;

				if (!_locked)
					_changed.execute(this);
			}
		}

		public function get w()	: Number
		{
			return _vector.w;
		}
		public function set w(value : Number) : void
		{
			if (value != _vector.w)
			{
				_vector.w = value;
				_update = UPDATE_ALL;

				if (!_locked)
					_changed.execute(this);
			}
		}

		public function get lengthSquared()	: Number
		{
			if (_update & UPDATE_LENGTH_SQ)
			{
				_update ^= UPDATE_LENGTH_SQ;
				_lengthSq = _vector.x * _vector.x + _vector.y * _vector.y
							+ _vector.z * _vector.z;
			}

			return _lengthSq;
		}

		public function get length() : Number
		{
			if (_update & UPDATE_LENGTH)
			{
				_update ^= UPDATE_LENGTH;
				_length = Math.sqrt(lengthSquared);
			}

			return _length;
		}

		public function get locked() : Boolean
		{
			return _locked;
		}

		public function get changed() : Signal
		{
			return _changed;
		}

		public function Vector4(x 	: Number	= 0.,
								y	: Number	= 0.,
								z	: Number	= 0.,
								w 	: Number	= 1.)
		{
			_vector.x = x;
			_vector.y = y;
			_vector.z = z;
			_vector.w = w;

			_update = UPDATE_ALL;
		}

		public static function add(u 	: Vector4,
								   v 	: Vector4,
								   out	: Vector4 = null) : Vector4
		{
			out ||= new Vector4();
			out.copyFrom(u);

			return out.incrementBy(v);
		}

		public static function subtract(u 	: Vector4,
										v 	: Vector4,
										out : Vector4 = null) : Vector4
		{
			out ||= new Vector4();
			out.copyFrom(u);

			return out.decrementBy(v);
		}

		public static function dotProduct(u : Vector4, v : Vector4) : Number
		{
			return u._vector.dotProduct(v._vector);
		}

		public static function crossProduct(u : Vector4, v : Vector4) : Vector4
		{
			return new Vector4(
				u._vector.y * v._vector.z - v._vector.y * u._vector.z,
				u._vector.z * v._vector.x - v._vector.z * u._vector.x,
				u._vector.x * v._vector.y - v._vector.x * u._vector.y
			);
		}

		public function compareTo(v : Vector4, allFour : Boolean = false) : Boolean
		{
			return _vector.x == v._vector.x
				   && _vector.y == v._vector.y
				   && _vector.z == v._vector.z
				   && (!allFour || (_vector.w == v._vector.w));
		}

		public static function distance(u : Vector4, v : Vector4) : Number
		{
			return Vector3D.distance(u._vector, v._vector);
		}

		public function copyFrom(source : Vector4) : Vector4
		{
			return set(source.x, source.y, source.z, source.w);
		}

		public function incrementBy(vector : Vector4) : Vector4
		{
			_vector.incrementBy(vector._vector);

			_update = UPDATE_ALL;
			_changed.execute(this);

			return this;
		}

		public function decrementBy(vector : Vector4) : Vector4
		{
			_vector.decrementBy(vector._vector);

			_update = UPDATE_ALL;
			_changed.execute(this);

			return this;
		}

		public function scaleBy(scale : Number) : Vector4
		{
			_vector.scaleBy(scale);

			_update = UPDATE_ALL;
			_changed.execute(this);

			return this;
		}

		public function lerp(target : Vector4, ratio : Number) : Vector4
		{
			if (ratio == 1.)
				return copyFrom(target);

			if (ratio != 0.)
			{
				var x : Number = _vector.x;
				var y : Number = _vector.y;
				var z : Number = _vector.z;
				var w : Number = _vector.w;

				set(
					x + (target._vector.x - x) * ratio,
					y + (target._vector.y - y) * ratio,
					z + (target._vector.z - z) * ratio,
					w + (target._vector.w - w) * ratio
				);
			}

			return this;
		}

		public function equals(v : Vector4, allFour : Boolean = false, tolerance : Number = 0.) : Boolean
		{
			return tolerance
				? _vector.nearEquals(v._vector, tolerance, allFour)
				: _vector.equals(v._vector, allFour);
		}

		public function project() : Vector4
		{
			_vector.x /= _vector.w;
			_vector.y /= _vector.w;
			_vector.z /= _vector.w;

			_update = UPDATE_ALL;
			_changed.execute(this);

			return this;
		}

		public function normalize() : Vector4
		{
			var l : Number = length;

			if (l != 0.)
			{
				_vector.x /= l;
				_vector.y /= l;
				_vector.z /= l;

				_length = 1.;
				_lengthSq = 1.;

				_update = UPDATE_NONE;
				_changed.execute(this);
			}

			return this;
		}

		public function crossProduct(vector : Vector4) : Vector4
		{
			var x : Number = _vector.x;
			var y : Number = _vector.y;
			var z : Number = _vector.z;

			_vector.x = y * vector._vector.z - vector._vector.y * z;
			_vector.y = z * vector._vector.x - vector._vector.z * x;
			_vector.z = x * vector._vector.y - vector._vector.x * y;

			_update = UPDATE_ALL;
			_changed.execute(this);

			return this;
		}

		public function getVector3D() : Vector3D
		{
			return _vector.clone();
		}

		public function set(x : Number,
							y : Number,
							z : Number,
							w : Number = 1.) : Vector4
		{
			if (x != _vector.x || y != _vector.y || z != _vector.z || w != _vector.w)
			{
				_vector.setTo(x, y, z);
				_vector.w = w;

				_update = UPDATE_ALL;
				if (!_locked)
					_changed.execute(this);
			}

			return this;
		}

		public function toString() : String
		{
			return '(' + _vector.x + ', ' + _vector.y + ', ' + _vector.z + ', ' + _vector.w + ')';
		}

		public static function scale(v : Vector4, s : Number, out : Vector4 = null) : Vector4
		{
			out ||= FACTORY.create() as Vector4;
			out.set(v._vector.x * s, v._vector.y * s, v._vector.z * s, v._vector.w * s);

			return out;
		}

		public function toVector3D(out : Vector3D = null) : Vector3D
		{
			if (!out)
				return _vector.clone();

			out.x = _vector.x;
			out.y = _vector.y;
			out.z = _vector.z;
			out.w = _vector.w;

			return out;
		}

		public function lock() : void
		{
			_locked = true;
		}

		public function unlock() : void
		{
			_locked = false;
			_changed.execute(this);
		}

		public function clone() : Vector4
		{
			return new Vector4(_vector.x, _vector.y, _vector.z, _vector.w);
		}
	}
}

fix Vector4::add() and Vector4::subtract()


package com.dotfold.dotvimstat.model
{
	/**
	 *
	 * @author jamesmcnamee
	 *
	 */
	public class LikeEntity extends BaseEntity
	{
		/**
		 *
		 */
		public function LikeEntity()
		{
			super();
		}

		public var description:String;
		public var duration:int;
		public var embed_privacy:String;
		public var height:int;
		public var liked_on:String;
		public var mobile_url:String;
		public var stats_number_of_comments:int;
		public var stats_number_of_likes:int;
		public var stats_number_of_plays:int;
		public var tags:String;
		public var thumbnail_large:String;
		public var thumbnail_medium:String;
		public var thumbnail_small:String;
		public var title:String;
		public var upload_date:int;
		public var url:String;
		public var user_id:int;
		public var user_name:String;
		public var user_portrait_huge:String;
		public var user_portrait_large:String;
		public var user_portrait_medium:String;
		public var user_portrait_small:String;
		public var user_url:String;
		public var width:int;

	}
}

package com.dotfold.dotvimstat.model
{
	/**
	 *
	 * @author jamesmcnamee
	 *
	 */
	public class LikeEntity extends BaseEntity
	{
		/**
		 *
		 */
		public function LikeEntity()
		{
			super();
		}

		public var description:String;
		public var duration:int;
		public var embed_privacy:String;
		public var height:int;
		public var liked_on:Date;
		public var mobile_url:String;
		public var stats_number_of_comments:int;
		public var stats_number_of_likes:int;
		public var stats_number_of_plays:int;
		public var tags:String;
		public var thumbnail_large:String;
		public var thumbnail_medium:String;
		public var thumbnail_small:String;
		public var title:String;
		public var upload_date:Date;
		public var url:String;
		public var user_id:int;
		public var user_name:String;
		public var user_portrait_huge:String;
		public var user_portrait_large:String;
		public var user_portrait_medium:String;
		public var user_portrait_small:String;
		public var user_url:String;
		public var width:int;

	}
}

use improved serialization for entity

[feat] use improved serialization for entity

- use real Date objects
- use Image collections


// Copyright: Hiroshi Ichikawa <http://gimite.net/en/>
// License: New BSD License
// Reference: http://dev.w3.org/html5/websockets/
// Reference: http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-31

package {

import flash.display.*;
import flash.events.*;
import flash.external.*;
import flash.net.*;
import flash.system.*;
import flash.utils.*;
import mx.core.*;
import mx.controls.*;
import mx.events.*;
import mx.utils.*;
import com.adobe.net.proxies.RFC2817Socket;
import com.hurlant.crypto.tls.TLSSocket;
import com.hurlant.crypto.tls.TLSConfig;
import com.hurlant.crypto.tls.TLSEngine;
import com.hurlant.crypto.tls.TLSSecurityParameters;
import com.gsolo.encryption.MD5;

[Event(name="message", type="WebSocketMessageEvent")]
[Event(name="open", type="flash.events.Event")]
[Event(name="close", type="flash.events.Event")]
[Event(name="error", type="flash.events.Event")]
[Event(name="stateChange", type="WebSocketStateEvent")]
public class WebSocket extends EventDispatcher {

  private static var CONNECTING:int = 0;
  private static var OPEN:int = 1;
  private static var CLOSING:int = 2;
  private static var CLOSED:int = 3;

  //private var rawSocket:RFC2817Socket;
  private var rawSocket:Socket;
  private var tlsSocket:TLSSocket;
  private var tlsConfig:TLSConfig;
  private var socket:Socket;
  private var main:WebSocketMain;
  private var url:String;
  private var scheme:String;
  private var host:String;
  private var port:uint;
  private var path:String;
  private var origin:String;
  private var protocol:String;
  private var buffer:ByteArray = new ByteArray();
  private var headerState:int = 0;
  private var readyState:int = CONNECTING;
  private var bufferedAmount:int = 0;
  private var headers:String;
  private var noiseChars:Array;
  private var expectedDigest:String;

  public function WebSocket(
      main:WebSocketMain, url:String, protocol:String,
      proxyHost:String = null, proxyPort:int = 0,
      headers:String = null) {
    this.main = main;
    initNoiseChars();
    this.url = url;
    var m:Array = url.match(/^(\w+):\/\/([^\/:]+)(:(\d+))?(\/.*)?$/);
    if (!m) main.fatal("SYNTAX_ERR: invalid url: " + url);
    this.scheme = m[1];
    this.host = m[2];
    this.port = parseInt(m[4] || "80");
    this.path = m[5] || "/";
    this.origin = main.getOrigin();
    this.protocol = protocol;
    // if present and not the empty string, headers MUST end with \r\n
    // headers should be zero or more complete lines, for example
    // "Header1: xxx\r\nHeader2: yyyy\r\n"
    this.headers = headers;

    /*
    socket = new RFC2817Socket();

    // if no proxy information is supplied, it acts like a normal Socket
    // @see RFC2817Socket::connect
    if (proxyHost != null && proxyPort != 0){
      socket.setProxyInfo(proxyHost, proxyPort);
    }
    */

    ExternalInterface.call("console.log", "[WebSocket] scheme: " + scheme);
    rawSocket = new Socket();

    rawSocket.addEventListener(Event.CLOSE, onSocketClose);
    rawSocket.addEventListener(Event.CONNECT, onSocketConnect);
    rawSocket.addEventListener(IOErrorEvent.IO_ERROR, onSocketIoError);
    rawSocket.addEventListener(SecurityErrorEvent.SECURITY_ERROR, onSocketSecurityError);
    if (scheme == "wss") {
        tlsConfig= new TLSConfig(TLSEngine.CLIENT,
            null, null, null, null, null,
            TLSSecurityParameters.PROTOCOL_VERSION);
        tlsConfig.trustSelfSignedCertificates = true;
        tlsConfig.ignoreCommonNameMismatch = true;

        tlsSocket = new TLSSocket();
        tlsSocket.addEventListener(ProgressEvent.SOCKET_DATA, onSocketData);
        socket = (tlsSocket as Socket);
    } else {
        rawSocket.addEventListener(ProgressEvent.SOCKET_DATA, onSocketData);
        socket = (rawSocket as Socket);
    }
    rawSocket.connect(host, port);
  }

  public function send(data:String):int {
    if (readyState == OPEN) {
      socket.writeByte(0x00);
      socket.writeUTFBytes(decodeURIComponent(data));
      socket.writeByte(0xff);
      socket.flush();
      main.log("sent: " + data);
      return -1;
    } else if (readyState == CLOSED) {
      var bytes:ByteArray = new ByteArray();
      bytes.writeUTFBytes(decodeURIComponent(data));
      bufferedAmount += bytes.length; // not sure whether it should include \x00 and \xff
      // We use return value to let caller know bufferedAmount because we cannot fire
      // stateChange event here which causes weird error:
      // > You are trying to call recursively into the Flash Player which is not allowed.
      return bufferedAmount;
    } else {
      main.fatal("INVALID_STATE_ERR: invalid state");
      return 0;
    }
  }

  public function close():void {
    main.log("close");
    try {
      socket.close();
    } catch (ex:Error) { }
    readyState = CLOSED;
    // We don't fire any events here because it causes weird error:
    // > You are trying to call recursively into the Flash Player which is not allowed.
    // We do something equivalent in JavaScript WebSocket#close instead.
  }

  public function getReadyState():int {
    return readyState;
  }

  public function getBufferedAmount():int {
    return bufferedAmount;
  }

  private function onSocketConnect(event:Event):void {
    main.log("connected");

    if (scheme == "wss") {
        ExternalInterface.call("console.log", "[WebSocket] starting SSL/TLS");
        tlsSocket.startTLS(rawSocket, host, tlsConfig);
    }

    var hostValue:String = host + (port == 80 ? "" : ":" + port);
    var cookie:String = "";
    if (main.getCallerHost() == host) {
      cookie = ExternalInterface.call("function(){return document.cookie}");
    }
    var key1:String = generateKey();
    var key2:String = generateKey();
    var key3:String = generateKey3();
    expectedDigest = getSecurityDigest(key1, key2, key3);
    var opt:String = "";
    if (protocol) opt += "WebSocket-Protocol: " + protocol + "\r\n";
    // if caller passes additional headers they must end with "\r\n"
    if (headers) opt += headers;

    var req:String = StringUtil.substitute(
      "GET {0} HTTP/1.1\r\n" +
      "Upgrade: WebSocket\r\n" +
      "Connection: Upgrade\r\n" +
      "Host: {1}\r\n" +
      "Origin: {2}\r\n" +
      "Cookie: {3}\r\n" +
      "Sec-WebSocket-Key1: {4}\r\n" +
      "Sec-WebSocket-Key2: {5}\r\n" +
      "{6}" +
      "\r\n",
      path, hostValue, origin, cookie, key1, key2, opt);
    main.log("request header:\n" + req);
    socket.writeUTFBytes(req);
    main.log("sent key3: " + key3);
    main.log("expected digest: " + expectedDigest);
    writeBytes(key3);
    socket.flush();
  }

  private function onSocketClose(event:Event):void {
    main.log("closed");
    readyState = CLOSED;
    notifyStateChange();
    dispatchEvent(new Event("close"));
  }

  private function onSocketIoError(event:IOErrorEvent):void {
    var message:String;
    if (readyState == CONNECTING) {
      message = "cannot connect to Web Socket server at " + url + " (IoError)";
    } else {
      message = "error communicating with Web Socket server at " + url + " (IoError)";
    }
    onError(message);
  }

  private function onSocketSecurityError(event:SecurityErrorEvent):void {
    var message:String;
    if (readyState == CONNECTING) {
      message =
          "cannot connect to Web Socket server at " + url + " (SecurityError)\n" +
          "make sure the server is running and Flash socket policy file is correctly placed";
    } else {
      message = "error communicating with Web Socket server at " + url + " (SecurityError)";
    }
    onError(message);
  }

  private function onError(message:String):void {
    var state:int = readyState;
    if (state == CLOSED) return;
    main.error(message);
    close();
    notifyStateChange();
    dispatchEvent(new Event(state == CONNECTING ? "close" : "error"));
  }

  private function onSocketData(event:ProgressEvent):void {
    var pos:int = buffer.length;
    socket.readBytes(buffer, pos);
    for (; pos < buffer.length; ++pos) {
      if (headerState < 4) {
        // try to find "\r\n\r\n"
        if ((headerState == 0 || headerState == 2) && buffer[pos] == 0x0d) {
          ++headerState;
        } else if ((headerState == 1 || headerState == 3) && buffer[pos] == 0x0a) {
          ++headerState;
        } else {
          headerState = 0;
        }
        if (headerState == 4) {
          var headerStr:String = buffer.readUTFBytes(pos + 1);
          main.log("response header:\n" + headerStr);
          if (!validateHeader(headerStr)) return;
          makeBufferCompact();
          pos = -1;
        }
      } else if (headerState == 4) {
        var replyDigest:String = readBytes(buffer, 16);
        main.log("reply digest: " + replyDigest);
        if (replyDigest != expectedDigest) {
          onError("digest doesn't match: " + replyDigest + " != " + expectedDigest);
          return;
        }
        headerState = 5;
        makeBufferCompact();
        pos = -1;
        readyState = OPEN;
        notifyStateChange();
        dispatchEvent(new Event("open"));
      } else {
        if (buffer[pos] == 0xff) {
          if (buffer.readByte() != 0x00) {
            onError("data must start with \\x00");
            return;
          }
          var data:String = buffer.readUTFBytes(pos - 1);
          main.log("received: " + data);
          dispatchEvent(new WebSocketMessageEvent("message", encodeURIComponent(data)));
          buffer.readByte();
          makeBufferCompact();
          pos = -1;
        }
      }
    }
  }

  private function validateHeader(headerStr:String):Boolean {
    var lines:Array = headerStr.split(/\r\n/);
    if (!lines[0].match(/^HTTP\/1.1 101 /)) {
      onError("bad response: " + lines[0]);
      return false;
    }
    var header:Object = {};
    for (var i:int = 1; i < lines.length; ++i) {
      if (lines[i].length == 0) continue;
      var m:Array = lines[i].match(/^(\S+): (.*)$/);
      if (!m) {
        onError("failed to parse response header line: " + lines[i]);
        return false;
      }
      header[m[1]] = m[2];
    }
    if (header["Upgrade"] != "WebSocket") {
      onError("invalid Upgrade: " + header["Upgrade"]);
      return false;
    }
    if (header["Connection"] != "Upgrade") {
      onError("invalid Connection: " + header["Connection"]);
      return false;
    }
    var resOrigin:String = header["Sec-WebSocket-Origin"].toLowerCase();
    if (resOrigin != origin) {
      onError("origin doesn't match: '" + resOrigin + "' != '" + origin + "'");
      return false;
    }
    if (protocol && header["Sec-WebSocket-Protocol"] != protocol) {
      onError("protocol doesn't match: '" +
        header["WebSocket-Protocol"] + "' != '" + protocol + "'");
      return false;
    }
    return true;
  }

  private function makeBufferCompact():void {
    if (buffer.position == 0) return;
    var nextBuffer:ByteArray = new ByteArray();
    buffer.readBytes(nextBuffer);
    buffer = nextBuffer;
  }

  private function notifyStateChange():void {
    dispatchEvent(new WebSocketStateEvent("stateChange", readyState, bufferedAmount));
  }

  private function initNoiseChars():void {
    noiseChars = new Array();
    for (var i:int = 0x21; i <= 0x2f; ++i) {
      noiseChars.push(String.fromCharCode(i));
    }
    for (var j:int = 0x3a; j <= 0x7a; ++j) {
      noiseChars.push(String.fromCharCode(j));
    }
  }

  private function generateKey():String {
    var spaces:uint = randomInt(1, 12);
    var max:uint = uint.MAX_VALUE / spaces;
    var number:uint = randomInt(0, max);
    var key:String = (number * spaces).toString();
    var noises:int = randomInt(1, 12);
    var pos:int;
    for (var i:int = 0; i < noises; ++i) {
      var char:String = noiseChars[randomInt(0, noiseChars.length - 1)];
      pos = randomInt(0, key.length);
      key = key.substr(0, pos) + char + key.substr(pos);
    }
    for (var j:int = 0; j < spaces; ++j) {
      pos = randomInt(1, key.length - 1);
      key = key.substr(0, pos) + " " + key.substr(pos);
    }
    return key;
  }

  private function generateKey3():String {
    var key3:String = "";
    for (var i:int = 0; i < 8; ++i) {
      key3 += String.fromCharCode(randomInt(0, 255));
    }
    return key3;
  }

  private function getSecurityDigest(key1:String, key2:String, key3:String):String {
    var bytes1:String = keyToBytes(key1);
    var bytes2:String = keyToBytes(key2);
    return MD5.rstr_md5(bytes1 + bytes2 + key3);
  }

  private function keyToBytes(key:String):String {
    var keyNum:uint = parseInt(key.replace(/[^\d]/g, ""));
    var spaces:uint = 0;
    for (var i:int = 0; i < key.length; ++i) {
      if (key.charAt(i) == " ") ++spaces;
    }
    var resultNum:uint = keyNum / spaces;
    var bytes:String = "";
    for (var j:int = 3; j >= 0; --j) {
      bytes += String.fromCharCode((resultNum >> (j * 8)) & 0xff);
    }
    return bytes;
  }

  private function writeBytes(bytes:String):void {
    for (var i:int = 0; i < bytes.length; ++i) {
      socket.writeByte(bytes.charCodeAt(i));
    }
  }

  private function readBytes(buffer:ByteArray, numBytes:int):String {
    var bytes:String = "";
    for (var i:int = 0; i < numBytes; ++i) {
      // & 0xff is to make \x80-\xff positive number.
      bytes += String.fromCharCode(buffer.readByte() & 0xff);
    }
    return bytes;
  }

  private function randomInt(min:uint, max:uint):uint {
    return min + Math.floor(Math.random() * (Number(max) - min + 1));
  }

  // for debug
  private function dumpBytes(bytes:String):void {
    var output:String = "";
    for (var i:int = 0; i < bytes.length; ++i) {
      output += bytes.charCodeAt(i).toString() + ", ";
    }
    main.log(output);
  }

}

}

// Copyright: Hiroshi Ichikawa <http://gimite.net/en/>
// License: New BSD License
// Reference: http://dev.w3.org/html5/websockets/
// Reference: http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-31

package {

import flash.display.*;
import flash.events.*;
import flash.external.*;
import flash.net.*;
import flash.system.*;
import flash.utils.*;
import mx.core.*;
import mx.controls.*;
import mx.events.*;
import mx.utils.*;
import com.adobe.net.proxies.RFC2817Socket;
import com.hurlant.crypto.tls.TLSSocket;
import com.hurlant.crypto.tls.TLSConfig;
import com.hurlant.crypto.tls.TLSEngine;
import com.hurlant.crypto.tls.TLSSecurityParameters;
import com.gsolo.encryption.MD5;

[Event(name="message", type="WebSocketMessageEvent")]
[Event(name="open", type="flash.events.Event")]
[Event(name="close", type="flash.events.Event")]
[Event(name="error", type="flash.events.Event")]
[Event(name="stateChange", type="WebSocketStateEvent")]
public class WebSocket extends EventDispatcher {

  private static var CONNECTING:int = 0;
  private static var OPEN:int = 1;
  private static var CLOSING:int = 2;
  private static var CLOSED:int = 3;

  //private var rawSocket:RFC2817Socket;
  private var rawSocket:Socket;
  private var tlsSocket:TLSSocket;
  private var tlsConfig:TLSConfig;
  private var socket:Socket;
  private var main:WebSocketMain;
  private var url:String;
  private var scheme:String;
  private var host:String;
  private var port:uint;
  private var path:String;
  private var origin:String;
  private var protocol:String;
  private var buffer:ByteArray = new ByteArray();
  private var headerState:int = 0;
  private var readyState:int = CONNECTING;
  private var bufferedAmount:int = 0;
  private var headers:String;
  private var noiseChars:Array;
  private var expectedDigest:String;

  public function WebSocket(
      main:WebSocketMain, url:String, protocol:String,
      proxyHost:String = null, proxyPort:int = 0,
      headers:String = null) {
    this.main = main;
    initNoiseChars();
    this.url = url;
    var m:Array = url.match(/^(\w+):\/\/([^\/:]+)(:(\d+))?(\/.*)?$/);
    if (!m) main.fatal("SYNTAX_ERR: invalid url: " + url);
    this.scheme = m[1];
    this.host = m[2];
    this.port = parseInt(m[4] || "80");
    this.path = m[5] || "/";
    this.origin = main.getOrigin();
    this.protocol = protocol;
    // if present and not the empty string, headers MUST end with \r\n
    // headers should be zero or more complete lines, for example
    // "Header1: xxx\r\nHeader2: yyyy\r\n"
    this.headers = headers;

    /*
    socket = new RFC2817Socket();

    // if no proxy information is supplied, it acts like a normal Socket
    // @see RFC2817Socket::connect
    if (proxyHost != null && proxyPort != 0){
      socket.setProxyInfo(proxyHost, proxyPort);
    }
    */

    ExternalInterface.call("console.log", "[WebSocket] scheme: " + scheme);
    rawSocket = new Socket();

    rawSocket.addEventListener(Event.CLOSE, onSocketClose);
    rawSocket.addEventListener(Event.CONNECT, onSocketConnect);
    rawSocket.addEventListener(IOErrorEvent.IO_ERROR, onSocketIoError);
    rawSocket.addEventListener(SecurityErrorEvent.SECURITY_ERROR, onSocketSecurityError);
    if (scheme == "wss") {
        tlsConfig= new TLSConfig(TLSEngine.CLIENT,
            null, null, null, null, null,
            TLSSecurityParameters.PROTOCOL_VERSION);
        tlsConfig.trustSelfSignedCertificates = true;
        tlsConfig.ignoreCommonNameMismatch = true;

        tlsSocket = new TLSSocket();
        tlsSocket.addEventListener(ProgressEvent.SOCKET_DATA, onSocketData);
        socket = (tlsSocket as Socket);
    } else {
        rawSocket.addEventListener(ProgressEvent.SOCKET_DATA, onSocketData);
        socket = (rawSocket as Socket);
    }
    rawSocket.connect(host, port);
  }

  public function send(data:String):int {
    if (readyState == OPEN) {
      socket.writeByte(0x00);
      socket.writeUTFBytes(decodeURIComponent(data));
      socket.writeByte(0xff);
      socket.flush();
      main.log("sent: " + data);
      return -1;
    } else if (readyState == CLOSED) {
      var bytes:ByteArray = new ByteArray();
      bytes.writeUTFBytes(decodeURIComponent(data));
      bufferedAmount += bytes.length; // not sure whether it should include \x00 and \xff
      // We use return value to let caller know bufferedAmount because we cannot fire
      // stateChange event here which causes weird error:
      // > You are trying to call recursively into the Flash Player which is not allowed.
      return bufferedAmount;
    } else {
      main.fatal("INVALID_STATE_ERR: invalid state");
      return 0;
    }
  }

  public function close():void {
    main.log("close");
    try {
      socket.close();
    } catch (ex:Error) { }
    readyState = CLOSED;
    // We don't fire any events here because it causes weird error:
    // > You are trying to call recursively into the Flash Player which is not allowed.
    // We do something equivalent in JavaScript WebSocket#close instead.
  }

  public function getReadyState():int {
    return readyState;
  }

  public function getBufferedAmount():int {
    return bufferedAmount;
  }

  private function onSocketConnect(event:Event):void {
    main.log("connected");

    if (scheme == "wss") {
        ExternalInterface.call("console.log", "[WebSocket] starting SSL/TLS");
        tlsSocket.startTLS(rawSocket, host, tlsConfig);
    }

    var hostValue:String = host + (port == 80 ? "" : ":" + port);
    var cookie:String = "";
    if (main.getCallerHost() == host) {
      cookie = ExternalInterface.call("function(){return document.cookie}");
    }
    var key1:String = generateKey();
    var key2:String = generateKey();
    var key3:String = generateKey3();
    expectedDigest = getSecurityDigest(key1, key2, key3);
    var opt:String = "";
    if (protocol) opt += "WebSocket-Protocol: " + protocol + "\r\n";
    // if caller passes additional headers they must end with "\r\n"
    if (headers) opt += headers;

    var req:String = StringUtil.substitute(
      "GET {0} HTTP/1.1\r\n" +
      "Upgrade: WebSocket\r\n" +
      "Connection: Upgrade\r\n" +
      "Host: {1}\r\n" +
      "Origin: {2}\r\n" +
      "Cookie: {3}\r\n" +
      "Sec-WebSocket-Key1: {4}\r\n" +
      "Sec-WebSocket-Key2: {5}\r\n" +
      "{6}" +
      "\r\n",
      path, hostValue, origin, cookie, key1, key2, opt);
    main.log("request header:\n" + req);
    socket.writeUTFBytes(req);
    main.log("sent key3: " + key3);
    writeBytes(key3);
    socket.flush();
  }

  private function onSocketClose(event:Event):void {
    main.log("closed");
    readyState = CLOSED;
    notifyStateChange();
    dispatchEvent(new Event("close"));
  }

  private function onSocketIoError(event:IOErrorEvent):void {
    var message:String;
    if (readyState == CONNECTING) {
      message = "cannot connect to Web Socket server at " + url + " (IoError)";
    } else {
      message = "error communicating with Web Socket server at " + url + " (IoError)";
    }
    onError(message);
  }

  private function onSocketSecurityError(event:SecurityErrorEvent):void {
    var message:String;
    if (readyState == CONNECTING) {
      message =
          "cannot connect to Web Socket server at " + url + " (SecurityError)\n" +
          "make sure the server is running and Flash socket policy file is correctly placed";
    } else {
      message = "error communicating with Web Socket server at " + url + " (SecurityError)";
    }
    onError(message);
  }

  private function onError(message:String):void {
    var state:int = readyState;
    if (state == CLOSED) return;
    main.error(message);
    close();
    notifyStateChange();
    dispatchEvent(new Event(state == CONNECTING ? "close" : "error"));
  }

  private function onSocketData(event:ProgressEvent):void {
    var pos:int = buffer.length;
    socket.readBytes(buffer, pos);
    for (; pos < buffer.length; ++pos) {
      if (headerState < 4) {
        // try to find "\r\n\r\n"
        if ((headerState == 0 || headerState == 2) && buffer[pos] == 0x0d) {
          ++headerState;
        } else if ((headerState == 1 || headerState == 3) && buffer[pos] == 0x0a) {
          ++headerState;
        } else {
          headerState = 0;
        }
        if (headerState == 4) {
          var headerStr:String = buffer.readUTFBytes(pos + 1);
          main.log("response header:\n" + headerStr);
          if (!validateHeader(headerStr)) return;
          makeBufferCompact();
          pos = -1;
        }
      } else if (headerState == 4) {
        if (pos == 15) {
          var replyDigest:String = readBytes(buffer, 16);
          main.log("reply digest: " + replyDigest);
          if (replyDigest != expectedDigest) {
            onError("digest doesn't match: " + replyDigest + " != " + expectedDigest);
            return;
          }
          headerState = 5;
          makeBufferCompact();
          pos = -1;
          readyState = OPEN;
          notifyStateChange();
          dispatchEvent(new Event("open"));
        }
      } else {
        if (buffer[pos] == 0xff) {
          if (buffer.readByte() != 0x00) {
            onError("data must start with \\x00");
            return;
          }
          var data:String = buffer.readUTFBytes(pos - 1);
          main.log("received: " + data);
          dispatchEvent(new WebSocketMessageEvent("message", encodeURIComponent(data)));
          buffer.readByte();
          makeBufferCompact();
          pos = -1;
        }
      }
    }
  }

  private function validateHeader(headerStr:String):Boolean {
    var lines:Array = headerStr.split(/\r\n/);
    if (!lines[0].match(/^HTTP\/1.1 101 /)) {
      onError("bad response: " + lines[0]);
      return false;
    }
    var header:Object = {};
    for (var i:int = 1; i < lines.length; ++i) {
      if (lines[i].length == 0) continue;
      var m:Array = lines[i].match(/^(\S+): (.*)$/);
      if (!m) {
        onError("failed to parse response header line: " + lines[i]);
        return false;
      }
      header[m[1]] = m[2];
    }
    if (header["Upgrade"] != "WebSocket") {
      onError("invalid Upgrade: " + header["Upgrade"]);
      return false;
    }
    if (header["Connection"] != "Upgrade") {
      onError("invalid Connection: " + header["Connection"]);
      return false;
    }
    var resOrigin:String = header["Sec-WebSocket-Origin"].toLowerCase();
    if (resOrigin != origin) {
      onError("origin doesn't match: '" + resOrigin + "' != '" + origin + "'");
      return false;
    }
    if (protocol && header["Sec-WebSocket-Protocol"] != protocol) {
      onError("protocol doesn't match: '" +
        header["WebSocket-Protocol"] + "' != '" + protocol + "'");
      return false;
    }
    return true;
  }

  private function makeBufferCompact():void {
    if (buffer.position == 0) return;
    var nextBuffer:ByteArray = new ByteArray();
    buffer.readBytes(nextBuffer);
    buffer = nextBuffer;
  }

  private function notifyStateChange():void {
    dispatchEvent(new WebSocketStateEvent("stateChange", readyState, bufferedAmount));
  }

  private function initNoiseChars():void {
    noiseChars = new Array();
    for (var i:int = 0x21; i <= 0x2f; ++i) {
      noiseChars.push(String.fromCharCode(i));
    }
    for (var j:int = 0x3a; j <= 0x7a; ++j) {
      noiseChars.push(String.fromCharCode(j));
    }
  }

  private function generateKey():String {
    var spaces:uint = randomInt(1, 12);
    var max:uint = uint.MAX_VALUE / spaces;
    var number:uint = randomInt(0, max);
    var key:String = (number * spaces).toString();
    var noises:int = randomInt(1, 12);
    var pos:int;
    for (var i:int = 0; i < noises; ++i) {
      var char:String = noiseChars[randomInt(0, noiseChars.length - 1)];
      pos = randomInt(0, key.length);
      key = key.substr(0, pos) + char + key.substr(pos);
    }
    for (var j:int = 0; j < spaces; ++j) {
      pos = randomInt(1, key.length - 1);
      key = key.substr(0, pos) + " " + key.substr(pos);
    }
    return key;
  }

  private function generateKey3():String {
    var key3:String = "";
    for (var i:int = 0; i < 8; ++i) {
      key3 += String.fromCharCode(randomInt(0, 255));
    }
    return key3;
  }

  private function getSecurityDigest(key1:String, key2:String, key3:String):String {
    var bytes1:String = keyToBytes(key1);
    var bytes2:String = keyToBytes(key2);
    return MD5.rstr_md5(bytes1 + bytes2 + key3);
  }

  private function keyToBytes(key:String):String {
    var keyNum:uint = parseInt(key.replace(/[^\d]/g, ""));
    var spaces:uint = 0;
    for (var i:int = 0; i < key.length; ++i) {
      if (key.charAt(i) == " ") ++spaces;
    }
    var resultNum:uint = keyNum / spaces;
    var bytes:String = "";
    for (var j:int = 3; j >= 0; --j) {
      bytes += String.fromCharCode((resultNum >> (j * 8)) & 0xff);
    }
    return bytes;
  }

  private function writeBytes(bytes:String):void {
    for (var i:int = 0; i < bytes.length; ++i) {
      socket.writeByte(bytes.charCodeAt(i));
    }
  }

  private function readBytes(buffer:ByteArray, numBytes:int):String {
    var bytes:String = "";
    for (var i:int = 0; i < numBytes; ++i) {
      // & 0xff is to make \x80-\xff positive number.
      bytes += String.fromCharCode(buffer.readByte() & 0xff);
    }
    return bytes;
  }

  private function randomInt(min:uint, max:uint):uint {
    return min + Math.floor(Math.random() * (Number(max) - min + 1));
  }

  // for debug
  private function dumpBytes(bytes:String):void {
    var output:String = "";
    for (var i:int = 0; i < bytes.length; ++i) {
      output += bytes.charCodeAt(i).toString() + ", ";
    }
    main.log(output);
  }

}

}

Revert "Remove invalid check of 'pos'."

Revert "Remove invalid check of 'pos'."

This reverts commit 79e646611ba35f38060b3dd42fbfcfaeb92253ee.


package org.mangui.HLS.utils {
  import com.hurlant.crypto.symmetric.AESKey;
  import com.hurlant.crypto.symmetric.CBCMode;
  import com.hurlant.crypto.symmetric.ICipher;
  import com.hurlant.crypto.symmetric.IPad;
  import com.hurlant.crypto.symmetric.IVMode;
  import com.hurlant.crypto.symmetric.NullPad;
  import com.hurlant.crypto.symmetric.PKCS5;

  import flash.utils.ByteArray;
  import flash.utils.Timer;
  import flash.events.Event;
  import flash.events.TimerEvent;

  /**
   * Contains Utility functions for Decryption
   */
  public class AES
  {
    private var _key:AESKey;
    private var _mode:ICipher;
    private var _iv:ByteArray;
    /* callback function upon read complete */
    private var _callback:Function;
    /** Timer for decrypting packets **/
    private var _timer:Timer;
    /** Byte data to be decrypt **/
    private var _data:ByteArray;
    /** Byte data to be decrypt **/
    private var _decrypteddata:ByteArray;
    /** read position **/
    private var _read_position:Number;
    /** chunk size to avoid blocking **/
    private static const CHUNK_SIZE:uint = 4096;
    /** is bytearray full ? **/
    private var _data_complete:Boolean;

    public function AES(key:ByteArray,iv:ByteArray) {
      var pad:IPad = new PKCS5;
      _key = new AESKey(key);
      _mode = new CBCMode(_key, pad);
      pad.setBlockSize(_mode.getBlockSize());
      _iv = iv;
      if (_mode is IVMode) {
        var ivmode:IVMode = _mode as IVMode;
        ivmode.IV = iv;
      }
    }

    public function decrypt(data:ByteArray,callback:Function):void {
      Log.debug("AES:async decrypt starting");
      _data = data;
      _data_complete = false;
      _callback = callback;
      _decrypteddata = new ByteArray();
      _read_position = 0;
      _timer = new Timer(0,0);
      _timer.addEventListener(TimerEvent.TIMER, _decryptData);
      _timer.start();
    }

    public function notifyappend():void {
      //Log.info("notify append");
      _timer.start();
    }

    public function notifycomplete():void {
      //Log.info("notify complete");
      _data_complete = true;
      _timer.start();
    }

    public function cancel():void {
      if(_timer) {
        _timer.stop();
        _timer = null;
      }
    }

    /** decrypt a small chunk of packets each time to avoid blocking **/
    private function _decryptData(e:Event):void {
      _data.position = _read_position;
      if(_data.bytesAvailable) {
        var dumpByteArray:ByteArray = new ByteArray();
        var newIv:ByteArray;
        var pad:IPad;
        if(_data.bytesAvailable <= CHUNK_SIZE) {
          if (_data_complete) {
            //Log.info("data complete, last chunk");
            pad = new PKCS5;
            _read_position+=_data.bytesAvailable;
            _data.readBytes(dumpByteArray,0,_data.bytesAvailable);
          } else {
            //Log.info("data not complete, stop timer");
            // data not complete, and available data less than chunk size, stop timer and return
            _timer.stop();
            return;
          }
        } else { // bytesAvailable > CHUNK_SIZE
          //Log.info("process chunk");
          pad = new NullPad;
          _read_position+=CHUNK_SIZE;
          _data.readBytes(dumpByteArray,0,CHUNK_SIZE);
          // Save new IV from ciphertext
          newIv = new ByteArray();
          dumpByteArray.position = (CHUNK_SIZE-16);
          dumpByteArray.readBytes(newIv, 0, 16);
        }
        dumpByteArray.position = 0;
        //Log.info("before decrypt");
        _mode = new CBCMode(_key, pad);
        pad.setBlockSize(_mode.getBlockSize());
        (_mode as IVMode).IV = _iv;
        _mode.decrypt(dumpByteArray);
        //Log.info("after decrypt");
        _decrypteddata.writeBytes(dumpByteArray);

        // switch IV to new one in case more bytes are available
        if(newIv) {
          _iv = newIv;
        }
      } else {
        //Log.info("no bytes available, stop timer");
        _timer.stop();
        if (_data_complete) {
          Log.debug("AES:data+decrypt completed, callback");
          _timer = null;
          // callback
          _decrypteddata.position=0;
          _callback(_decrypteddata);
        }
      }
    }

    public function destroy():void {
      _key.dispose();
      _key = null;
      _mode = null;
    }
  }
}

package org.mangui.HLS.utils {
  import com.hurlant.crypto.symmetric.AESKey;
  import com.hurlant.crypto.symmetric.CBCMode;
  import com.hurlant.crypto.symmetric.ICipher;
  import com.hurlant.crypto.symmetric.IPad;
  import com.hurlant.crypto.symmetric.IVMode;
  import com.hurlant.crypto.symmetric.NullPad;
  import com.hurlant.crypto.symmetric.PKCS5;

  import flash.utils.ByteArray;
  import flash.utils.Timer;
  import flash.events.Event;
  import flash.events.TimerEvent;

  /**
   * Contains Utility functions for Decryption
   */
  public class AES
  {
    private var _key:AESKey;
    private var _mode:ICipher;
    private var _iv:ByteArray;
    /* callback function upon read complete */
    private var _callback:Function;
    /** Timer for decrypting packets **/
    private var _timer:Timer;
    /** Byte data to be decrypt **/
    private var _data:ByteArray;
    /** Byte data to be decrypt **/
    private var _decrypteddata:ByteArray;
    /** read position **/
    private var _read_position:Number;
    /** chunk size to avoid blocking **/
    private static const CHUNK_SIZE:uint = 2048;
    /** is bytearray full ? **/
    private var _data_complete:Boolean;

    public function AES(key:ByteArray,iv:ByteArray) {
      var pad:IPad = new PKCS5;
      _key = new AESKey(key);
      _mode = new CBCMode(_key, pad);
      pad.setBlockSize(_mode.getBlockSize());
      _iv = iv;
      if (_mode is IVMode) {
        var ivmode:IVMode = _mode as IVMode;
        ivmode.IV = iv;
      }
    }

    public function decrypt(data:ByteArray,callback:Function):void {
      Log.debug("AES:async decrypt starting");
      _data = data;
      _data_complete = false;
      _callback = callback;
      _decrypteddata = new ByteArray();
      _read_position = 0;
      _timer = new Timer(0,0);
      _timer.addEventListener(TimerEvent.TIMER, _decryptTimer);
      _timer.start();
    }

    public function notifyappend():void {
      //Log.info("notify append");
      _timer.start();
    }

    public function notifycomplete():void {
      //Log.info("notify complete");
      _data_complete = true;
      _timer.start();
    }

    public function cancel():void {
      if(_timer) {
        _timer.stop();
        _timer = null;
      }
    }

    private function _decryptTimer(e:Event):void {
      var start_time:Number = new Date().getTime();
      do {
         _decryptData();
      // dont spend more than 20 ms in the decrypt timer to avoid blocking/freezing video
      } while (_timer.running && new Date().getTime() - start_time < 20);
    }

    /** decrypt a small chunk of packets each time to avoid blocking **/
    private function _decryptData():void {
      _data.position = _read_position;
      if(_data.bytesAvailable) {
        var dumpByteArray:ByteArray = new ByteArray();
        var newIv:ByteArray;
        var pad:IPad;
        if(_data.bytesAvailable <= CHUNK_SIZE) {
          if (_data_complete) {
            //Log.info("data complete, last chunk");
            pad = new PKCS5;
            _read_position+=_data.bytesAvailable;
            _data.readBytes(dumpByteArray,0,_data.bytesAvailable);
          } else {
            //Log.info("data not complete, stop timer");
            // data not complete, and available data less than chunk size, stop timer and return
            _timer.stop();
            return;
          }
        } else { // bytesAvailable > CHUNK_SIZE
          //Log.info("process chunk");
          pad = new NullPad;
          _read_position+=CHUNK_SIZE;
          _data.readBytes(dumpByteArray,0,CHUNK_SIZE);
          // Save new IV from ciphertext
          newIv = new ByteArray();
          dumpByteArray.position = (CHUNK_SIZE-16);
          dumpByteArray.readBytes(newIv, 0, 16);
        }
        dumpByteArray.position = 0;
        //Log.info("before decrypt");
        _mode = new CBCMode(_key, pad);
        pad.setBlockSize(_mode.getBlockSize());
        (_mode as IVMode).IV = _iv;
        _mode.decrypt(dumpByteArray);
        //Log.info("after decrypt");
        _decrypteddata.writeBytes(dumpByteArray);

        // switch IV to new one in case more bytes are available
        if(newIv) {
          _iv = newIv;
        }
      } else {
        //Log.info("no bytes available, stop timer");
        _timer.stop();
        if (_data_complete) {
          Log.debug("AES:data+decrypt completed, callback");
          // callback
          _decrypteddata.position=0;
          _callback(_decrypteddata);
        }
      }
    }

    public function destroy():void {
      _key.dispose();
      _key = null;
      _mode = null;
    }
  }
}

optimize AES decryption time from 1.8Mb/s to 3.6Mb/s on Core-i5 2410M @2.30 GHz the bottleneck was the timer not restarting quickly. to workaround this, we continue to decrypt chunk of data until we reach a duration of 20ms. related to https://github.com/mangui/HLSprovider/issues/55