package aerys.minko.type.data
{
	import aerys.minko.type.Signal;

	import flash.utils.Dictionary;

	public class DataBindings
	{
		private var _bindingNames					: Vector.<String>	= new Vector.<String>();

		private var _bindingNameToValue				: Object			= {};
		private var _bindingNameToChangedSignal		: Object			= {};

		private var _providerToBindingNames			: Dictionary		= new Dictionary(); // dic[Vector.<String>[]]
		private var _attributeToProviders			: Dictionary		= new Dictionary(); // dic[Vector.<IDataProvider>[]]
		private var _attributeToProvidersAttrNames	: Dictionary		= new Dictionary(); // dic[Vector.<String>[]]

		public function get numProperties() : uint
		{
			return _bindingNames.length;
		}

		public function DataBindings()
		{
		}

		public function add(provider : IDataProvider) : void
		{
			if (_providerToBindingNames[provider])
				throw new Error('This provider is already binded');

			var providerBindingNames	: Vector.<String>	= new Vector.<String>();
			var dataDescriptor			: Object			= provider.dataDescriptor;

			provider.changed.add(onProviderChange);

			for (var attrName : String in dataDescriptor)
			{
				// if this provider attribute is also a dataprovider, let's also bind it
				var bindingName	: String		= dataDescriptor[attrName];
				var attribute	: Object		= provider[attrName]
				var dpAttribute	: IDataProvider	= attribute as IDataProvider;

				if (_bindingNames.indexOf(bindingName) != -1)
					throw new Error('Another Dataprovider is already declaring "' + bindingName + '". Cannot overwrite.');

				if (dpAttribute != null)
				{
					dpAttribute.changed.add(onProviderAttributeChange);

					if (!_attributeToProviders[dpAttribute])
						_attributeToProviders[dpAttribute] = new <IDataProvider>[];

					if (!_attributeToProvidersAttrNames[dpAttribute])
						_attributeToProvidersAttrNames[dpAttribute] = new <String>[];

					_attributeToProviders[dpAttribute].push(provider);
					_attributeToProvidersAttrNames[dpAttribute].push(attrName);
				}

				_bindingNameToValue[bindingName] = attribute;

				providerBindingNames.push(bindingName);
				_bindingNames.push(bindingName);

				getPropertyChangedSignal(bindingName).execute(this, bindingName, attribute);
			}

			_providerToBindingNames[provider] = providerBindingNames;
		}

		public function remove(provider : IDataProvider) : void
		{
			var bindingNames : Vector.<String> = _providerToBindingNames[provider];

			if (bindingNames == null)
				throw new ArgumentError('No such provider was binded');

			for each (var bindingName : String in bindingNames)
			{
				var indexOf : int = _bindingNames.indexOf(bindingName);

				_bindingNames.splice(indexOf, 1);

				if (_bindingNameToValue[bindingName] is IDataProvider)
					IDataProvider(_bindingNameToValue[bindingName]).changed.remove(onProviderAttributeChange);

				delete _bindingNameToValue[bindingName];
			}

			var attributesToDelete : Vector.<Object> = new Vector.<Object>();

			for (var attribute : Object in _attributeToProviders)
			{
				var providers		: Vector.<IDataProvider>	= _attributeToProviders[attribute];
				var attrNames		: Vector.<String>			= _attributeToProvidersAttrNames[attribute];
				var indexOfProvider	: int						= providers.indexOf(provider);

				if (indexOfProvider != -1)
				{
					providers.splice(indexOfProvider, 1);
					attrNames.splice(indexOfProvider, 1);
				}

				if (providers.length == 0)
					attributesToDelete.push(attribute);
			}

			for (var attributeToDelete : Object in attributesToDelete)
			{
				delete _attributeToProviders[attributeToDelete];
				delete _attributeToProvidersAttrNames[attributeToDelete];
			}

			provider.changed.remove(onProviderChange);

			delete _providerToBindingNames[provider];

			for each (bindingName in bindingNames)
			{
				trace(bindingName);
				getPropertyChangedSignal(bindingName).execute(this, bindingName, null);
			}
		}

		public function getPropertyChangedSignal(bindingName : String) : Signal
		{
			if (!_bindingNameToChangedSignal[bindingName])
				_bindingNameToChangedSignal[bindingName] = new Signal('DataBindings.changed[' + bindingName + ']');

			return _bindingNameToChangedSignal[bindingName];
		}

		public function propertyExists(bindingName : String) : Boolean
		{
			return _bindingNameToValue.hasOwnProperty(bindingName);
		}

		public function getProperty(bindingName : String) : *
		{
			return _bindingNameToValue[bindingName];
		}

		public function getPropertyName(bindingIndex : uint) : String
		{
			if (bindingIndex > numProperties)
				throw new ArgumentError('No such binding');

			return _bindingNames[bindingIndex];
		}

		/**
		 * A provider attribute has changed, and the provider tells us.
		 * For example, camera.fov has changed, the camera dispatches a 'changed' signal with 'fov' as attributeName.
		 *
		 * It could also be that camera.localToWorld has been replaced by another matrix instance.
		 */
		private function onProviderChange(source : IDataProvider, attributeName : String) : void
		{
			if (attributeName == null)
			{
				throw new Error('DataProviders must change one property at a time.');
			}
			else if (attributeName == 'dataDescriptor')
			{
				remove(source);
				add(source);
			}
			else
			{
				var bindingName : String		= source.dataDescriptor[attributeName];
				var oldDpValue	: IDataProvider	= _bindingNameToValue[bindingName] as IDataProvider;
				var newValue	: Object		= source[attributeName];
				var newDpValue	: IDataProvider	= newValue as IDataProvider;

				// we are replacing a data provider. We must remove listeners and related mapping keys
				if (oldDpValue != null)
				{
					oldDpValue.changed.remove(onProviderAttributeChange);

					var providers	: Vector.<IDataProvider>	= _attributeToProviders[oldDpValue];
					var attrNames	: Vector.<String>			= _attributeToProvidersAttrNames[oldDpValue];

					if (providers.length == 1)
					{
						delete _attributeToProviders[oldDpValue];
						delete _attributeToProvidersAttrNames[oldDpValue];
					}
					else
					{
						var index : uint = providers.indexOf(source);
						providers.splice(index, 1);
						attrNames.splice(index, 1);
					}
				}

				// the new value for this key is a dataprovider, we must listen changes.
				if (newDpValue != null)
				{
					newDpValue.changed.add(onProviderAttributeChange);

					if (!_attributeToProviders[newDpValue])
						_attributeToProviders[newDpValue] = new <IDataProvider>[];

					if (!_attributeToProvidersAttrNames[newDpValue])
						_attributeToProvidersAttrNames[newDpValue] = new <String>[];

					_attributeToProviders[newDpValue].push(source);
					_attributeToProvidersAttrNames[newDpValue].push(attributeName);
				}

				_bindingNameToValue[bindingName] = newValue;

				getPropertyChangedSignal(bindingName).execute(this, bindingName, newValue);
			}
		}

		/**
		 * A provider attribute has changed, and the attribute tells us.
		 * For example, camera.localToWorld has been updated.
		 */
		private function onProviderAttributeChange(source : IDataProvider, key : String) : void
		{
			var providers		: Vector.<IDataProvider>	= _attributeToProviders[source];
			var attrNames		: Vector.<String>			= _attributeToProvidersAttrNames[source];
			var numProviders	: uint						= providers.length;

			for (var providerId : uint = 0; providerId < numProviders; ++providerId)
			{
				var provider	: IDataProvider	= providers[providerId];
				var attrName	: String		= attrNames[providerId];
				var bindingName : String		= provider.dataDescriptor[attrName];

				getPropertyChangedSignal(bindingName).execute(this, bindingName, source);
			}
		}
	}
}

package aerys.minko.type.data
{
	import aerys.minko.type.Signal;

	import flash.utils.Dictionary;

	public class DataBindings
	{
		private var _bindingNames					: Vector.<String>	= new Vector.<String>();

		private var _bindingNameToValue				: Object			= {};
		private var _bindingNameToChangedSignal		: Object			= {};

		private var _providerToBindingNames			: Dictionary		= new Dictionary(); // dic[Vector.<String>[]]
		private var _attributeToProviders			: Dictionary		= new Dictionary(); // dic[Vector.<IDataProvider>[]]
		private var _attributeToProvidersAttrNames	: Dictionary		= new Dictionary(); // dic[Vector.<String>[]]

		public function get numProperties() : uint
		{
			return _bindingNames.length;
		}

		public function DataBindings()
		{
		}

		public function add(provider : IDataProvider) : void
		{
			if (_providerToBindingNames[provider])
				throw new Error('This provider is already binded');

			var providerBindingNames	: Vector.<String>	= new Vector.<String>();
			var dataDescriptor			: Object			= provider.dataDescriptor;

			provider.changed.add(onProviderChange);

			for (var attrName : String in dataDescriptor)
			{
				// if this provider attribute is also a dataprovider, let's also bind it
				var bindingName	: String		= dataDescriptor[attrName];
				var attribute	: Object		= provider[attrName]
				var dpAttribute	: IDataProvider	= attribute as IDataProvider;

				if (_bindingNames.indexOf(bindingName) != -1)
					throw new Error('Another Dataprovider is already declaring "' + bindingName + '". Cannot overwrite.');

				if (dpAttribute != null)
				{
					dpAttribute.changed.add(onProviderAttributeChange);

					if (!_attributeToProviders[dpAttribute])
						_attributeToProviders[dpAttribute] = new <IDataProvider>[];

					if (!_attributeToProvidersAttrNames[dpAttribute])
						_attributeToProvidersAttrNames[dpAttribute] = new <String>[];

					_attributeToProviders[dpAttribute].push(provider);
					_attributeToProvidersAttrNames[dpAttribute].push(attrName);
				}

				_bindingNameToValue[bindingName] = attribute;

				providerBindingNames.push(bindingName);
				_bindingNames.push(bindingName);

				getPropertyChangedSignal(bindingName).execute(this, bindingName, attribute);
			}

			_providerToBindingNames[provider] = providerBindingNames;
		}

		public function remove(provider : IDataProvider) : void
		{
			var bindingNames : Vector.<String> = _providerToBindingNames[provider];

			if (bindingNames == null)
				throw new ArgumentError('No such provider was binded');

			for each (var bindingName : String in bindingNames)
			{
				var indexOf : int = _bindingNames.indexOf(bindingName);

				_bindingNames.splice(indexOf, 1);

				if (_bindingNameToValue[bindingName] is IDataProvider)
					IDataProvider(_bindingNameToValue[bindingName]).changed.remove(onProviderAttributeChange);

				delete _bindingNameToValue[bindingName];
			}

			var attributesToDelete : Vector.<Object> = new Vector.<Object>();

			for (var attribute : Object in _attributeToProviders)
			{
				var providers		: Vector.<IDataProvider>	= _attributeToProviders[attribute];
				var attrNames		: Vector.<String>			= _attributeToProvidersAttrNames[attribute];
				var indexOfProvider	: int						= providers.indexOf(provider);

				if (indexOfProvider != -1)
				{
					providers.splice(indexOfProvider, 1);
					attrNames.splice(indexOfProvider, 1);
				}

				if (providers.length == 0)
					attributesToDelete.push(attribute);
			}

			for (var attributeToDelete : Object in attributesToDelete)
			{
				delete _attributeToProviders[attributeToDelete];
				delete _attributeToProvidersAttrNames[attributeToDelete];
			}

			provider.changed.remove(onProviderChange);

			delete _providerToBindingNames[provider];

			for each (bindingName in bindingNames)
				getPropertyChangedSignal(bindingName).execute(this, bindingName, null);
		}

		public function getPropertyChangedSignal(bindingName : String) : Signal
		{
			if (!_bindingNameToChangedSignal[bindingName])
				_bindingNameToChangedSignal[bindingName] = new Signal('DataBindings.changed[' + bindingName + ']');

			return _bindingNameToChangedSignal[bindingName];
		}

		public function propertyExists(bindingName : String) : Boolean
		{
			return _bindingNameToValue.hasOwnProperty(bindingName);
		}

		public function getProperty(bindingName : String) : *
		{
			return _bindingNameToValue[bindingName];
		}

		public function getPropertyName(bindingIndex : uint) : String
		{
			if (bindingIndex > numProperties)
				throw new ArgumentError('No such binding');

			return _bindingNames[bindingIndex];
		}

		/**
		 * A provider attribute has changed, and the provider tells us.
		 * For example, camera.fov has changed, the camera dispatches a 'changed' signal with 'fov' as attributeName.
		 *
		 * It could also be that camera.localToWorld has been replaced by another matrix instance.
		 */
		private function onProviderChange(source : IDataProvider, attributeName : String) : void
		{
			if (attributeName == null)
			{
				throw new Error('DataProviders must change one property at a time.');
			}
			else if (attributeName == 'dataDescriptor')
			{
				remove(source);
				add(source);
			}
			else
			{
				var bindingName : String		= source.dataDescriptor[attributeName];
				var oldDpValue	: IDataProvider	= _bindingNameToValue[bindingName] as IDataProvider;
				var newValue	: Object		= source[attributeName];
				var newDpValue	: IDataProvider	= newValue as IDataProvider;

				// we are replacing a data provider. We must remove listeners and related mapping keys
				if (oldDpValue != null)
				{
					oldDpValue.changed.remove(onProviderAttributeChange);

					var providers	: Vector.<IDataProvider>	= _attributeToProviders[oldDpValue];
					var attrNames	: Vector.<String>			= _attributeToProvidersAttrNames[oldDpValue];

					if (providers.length == 1)
					{
						delete _attributeToProviders[oldDpValue];
						delete _attributeToProvidersAttrNames[oldDpValue];
					}
					else
					{
						var index : uint = providers.indexOf(source);
						providers.splice(index, 1);
						attrNames.splice(index, 1);
					}
				}

				// the new value for this key is a dataprovider, we must listen changes.
				if (newDpValue != null)
				{
					newDpValue.changed.add(onProviderAttributeChange);

					if (!_attributeToProviders[newDpValue])
						_attributeToProviders[newDpValue] = new <IDataProvider>[];

					if (!_attributeToProvidersAttrNames[newDpValue])
						_attributeToProvidersAttrNames[newDpValue] = new <String>[];

					_attributeToProviders[newDpValue].push(source);
					_attributeToProvidersAttrNames[newDpValue].push(attributeName);
				}

				_bindingNameToValue[bindingName] = newValue;

				getPropertyChangedSignal(bindingName).execute(this, bindingName, newValue);
			}
		}

		/**
		 * A provider attribute has changed, and the attribute tells us.
		 * For example, camera.localToWorld has been updated.
		 */
		private function onProviderAttributeChange(source : IDataProvider, key : String) : void
		{
			var providers		: Vector.<IDataProvider>	= _attributeToProviders[source];
			var attrNames		: Vector.<String>			= _attributeToProvidersAttrNames[source];
			var numProviders	: uint						= providers.length;

			for (var providerId : uint = 0; providerId < numProviders; ++providerId)
			{
				var provider	: IDataProvider	= providers[providerId];
				var attrName	: String		= attrNames[providerId];
				var bindingName : String		= provider.dataDescriptor[attrName];

				getPropertyChangedSignal(bindingName).execute(this, bindingName, source);
			}
		}
	}
}

remove debug call to trace() in DataBindings.remove()


// Urho3D editor node transform gizmo handling

Node@ gizmoNode;
StaticModel@ gizmo;

const float axisMaxD = 0.1;
const float axisMaxT = 1.0;
const float rotSensitivity = 50.0;

EditMode lastGizmoMode;

// For undo
bool previousGizmoDrag;
bool needGizmoUndo;
Array<Transform> oldGizmoTransforms;

class GizmoAxis
{
    Ray axisRay;
    bool selected;
    bool lastSelected;
    float t;
    float d;
    float lastT;
    float lastD;

    GizmoAxis()
    {
        selected = false;
        lastSelected = false;
        t = 0.0;
        d = 0.0;
        lastT = 0.0;
        lastD = 0.0;
    }

    void Update(Ray cameraRay, float scale, bool drag)
    {
        // Do not select when UI has modal element
        if (ui.HasModalElement())
        {
            selected = false;
            return;
        }

        Vector3 closest = cameraRay.ClosestPoint(axisRay);
        Vector3 projected = axisRay.Project(closest);
        d = axisRay.Distance(closest);
        t = (projected - axisRay.origin).DotProduct(axisRay.direction);

        // Determine the sign of d from a plane that goes through the camera position to the axis
        Plane axisPlane(cameraNode.position, axisRay.origin, axisRay.origin + axisRay.direction);
        if (axisPlane.Distance(closest) < 0.0)
            d = -d;

        // Update selected status only when not dragging
        if (!drag)
        {
            selected = Abs(d) < axisMaxD * scale && t >= -axisMaxD * scale && t <= axisMaxT * scale;
            lastT = t;
            lastD = d;
        }
    }

    void Moved()
    {
        lastT = t;
        lastD = d;
    }
}

GizmoAxis gizmoAxisX;
GizmoAxis gizmoAxisY;
GizmoAxis gizmoAxisZ;

void CreateGizmo()
{
    gizmoNode = Node();
    gizmo = gizmoNode.CreateComponent("StaticModel");
    gizmo.model = cache.GetResource("Model", "Models/Editor/Axes.mdl");
    gizmo.materials[0] = cache.GetResource("Material", "Materials/Editor/RedUnlit.xml");
    gizmo.materials[1] = cache.GetResource("Material", "Materials/Editor/GreenUnlit.xml");
    gizmo.materials[2] = cache.GetResource("Material", "Materials/Editor/BlueUnlit.xml");
    gizmo.enabled = false;
    gizmo.viewMask = 0x80000000; // Editor raycasts use viewmask 0x7fffffff
    gizmo.occludee = false;

    gizmoAxisX.lastSelected = false;
    gizmoAxisY.lastSelected = false;
    gizmoAxisZ.lastSelected = false;
    lastGizmoMode = EDIT_MOVE;
}

void HideGizmo()
{
    if (gizmo !is null)
        gizmo.enabled = false;
}

void ShowGizmo()
{
    if (gizmo !is null)
    {
        gizmo.enabled = true;

        // Because setting enabled = false detaches the gizmo from octree,
        // and it is a manually added drawable, must readd to octree when showing
        if (editorScene.octree !is null)
            editorScene.octree.AddManualDrawable(gizmo);
    }
}

void UpdateGizmo()
{
    UseGizmo();
    PositionGizmo();
    ResizeGizmo();
}

void PositionGizmo()
{
    if (gizmo is null)
        return;

    Vector3 center(0, 0, 0);
    bool containsScene = false;

    for (uint i = 0; i < editNodes.length; ++i)
    {
        // Scene's transform should not be edited, so hide gizmo if it is included
        if (editNodes[i] is editorScene)
        {
            containsScene = true;
            break;
        }
        center += editNodes[i].worldPosition;
    }

    if (editNodes.empty || containsScene)
    {
        HideGizmo();
        return;
    }

    center /= editNodes.length;
    gizmoNode.position = center;

    if (axisMode == AXIS_WORLD || editNodes.length > 1)
        gizmoNode.rotation = Quaternion();
    else
        gizmoNode.rotation = editNodes[0].worldRotation;

    if (editMode != lastGizmoMode)
    {
        switch (editMode)
        {
        case EDIT_MOVE:
            gizmo.model = cache.GetResource("Model", "Models/Editor/Axes.mdl");
            break;

        case EDIT_ROTATE:
            gizmo.model = cache.GetResource("Model", "Models/Editor/RotateAxes.mdl");
            break;

        case EDIT_SCALE:
            gizmo.model = cache.GetResource("Model", "Models/Editor/ScaleAxes.mdl");
            break;
        }

        lastGizmoMode = editMode;
    }

    if ((editMode != EDIT_SELECT && !orbiting) && !gizmo.enabled)
        ShowGizmo();
    else if ((editMode == EDIT_SELECT || orbiting) && gizmo.enabled)
        HideGizmo();
}

void ResizeGizmo()
{
    if (gizmo is null || !gizmo.enabled)
        return;

    float scale = 0.1;

    if (camera.orthographic)
        scale *= camera.orthoSize;
    else
        scale *= (camera.view.Inverse() * gizmoNode.position).z;

    gizmoNode.scale = Vector3(scale, scale, scale);
}

void CalculateGizmoAxes()
{
    gizmoAxisX.axisRay = Ray(gizmoNode.position, gizmoNode.rotation * Vector3(1, 0, 0));
    gizmoAxisY.axisRay = Ray(gizmoNode.position, gizmoNode.rotation * Vector3(0, 1, 0));
    gizmoAxisZ.axisRay = Ray(gizmoNode.position, gizmoNode.rotation * Vector3(0, 0, 1));
}

void GizmoMoved()
{
    gizmoAxisX.Moved();
    gizmoAxisY.Moved();
    gizmoAxisZ.Moved();
}

void UseGizmo()
{
    if (gizmo is null || !gizmo.enabled || editMode == EDIT_SELECT)
    {
        StoreGizmoEditActions();
        previousGizmoDrag = false;
        return;
    }

    IntVector2 pos = ui.cursorPosition;
    if (ui.GetElementAt(pos) !is null)
        return;
    Ray cameraRay = GetActiveViewportCameraRay();
    float scale = gizmoNode.scale.x;

    // Recalculate axes only when not left-dragging
    bool drag = input.mouseButtonDown[MOUSEB_LEFT];
    if (!drag)
        CalculateGizmoAxes();

    gizmoAxisX.Update(cameraRay, scale, drag);
    gizmoAxisY.Update(cameraRay, scale, drag);
    gizmoAxisZ.Update(cameraRay, scale, drag);

    if (gizmoAxisX.selected != gizmoAxisX.lastSelected)
    {
        gizmo.materials[0] = cache.GetResource("Material", gizmoAxisX.selected ? "Materials/Editor/BrightRedUnlit.xml" :
            "Materials/Editor/RedUnlit.xml");
        gizmoAxisX.lastSelected = gizmoAxisX.selected;
    }
    if (gizmoAxisY.selected != gizmoAxisY.lastSelected)
    {
        gizmo.materials[1] = cache.GetResource("Material", gizmoAxisY.selected ? "Materials/Editor/BrightGreenUnlit.xml" :
            "Materials/Editor/GreenUnlit.xml");
        gizmoAxisY.lastSelected = gizmoAxisY.selected;
    }
    if (gizmoAxisZ.selected != gizmoAxisZ.lastSelected)
    {
        gizmo.materials[2] = cache.GetResource("Material", gizmoAxisZ.selected ? "Materials/Editor/BrightBlueUnlit.xml" :
            "Materials/Editor/BlueUnlit.xml");
        gizmoAxisZ.lastSelected = gizmoAxisZ.selected;
    };

    if (drag)
    {
        // Store initial transforms for undo when gizmo drag started
        if (!previousGizmoDrag)
        {
            oldGizmoTransforms.Resize(editNodes.length);
            for (uint i = 0; i < editNodes.length; ++i)
                oldGizmoTransforms[i].Define(editNodes[i]);
        }

        bool moved = false;

        if (editMode == EDIT_MOVE)
        {
            Vector3 adjust(0, 0, 0);
            if (gizmoAxisX.selected)
                adjust += Vector3(1, 0, 0) * (gizmoAxisX.t - gizmoAxisX.lastT);
            if (gizmoAxisY.selected)
                adjust += Vector3(0, 1, 0) * (gizmoAxisY.t - gizmoAxisY.lastT);
            if (gizmoAxisZ.selected)
                adjust += Vector3(0, 0, 1) * (gizmoAxisZ.t - gizmoAxisZ.lastT);

            moved = MoveNodes(adjust);
        }
        else if (editMode == EDIT_ROTATE)
        {
            Vector3 adjust(0, 0, 0);
            if (gizmoAxisX.selected)
                adjust.x = (gizmoAxisX.d - gizmoAxisX.lastD) * rotSensitivity / scale;
            if (gizmoAxisY.selected)
                adjust.y = -(gizmoAxisY.d - gizmoAxisY.lastD) * rotSensitivity / scale;
            if (gizmoAxisZ.selected)
                adjust.z = (gizmoAxisZ.d - gizmoAxisZ.lastD) * rotSensitivity / scale;

            moved = RotateNodes(adjust);
        }
        else if (editMode == EDIT_SCALE)
        {
            Vector3 adjust(0, 0, 0);
            if (gizmoAxisX.selected)
                adjust += Vector3(1, 0, 0) * (gizmoAxisX.t - gizmoAxisX.lastT);
            if (gizmoAxisY.selected)
                adjust += Vector3(0, 1, 0) * (gizmoAxisY.t - gizmoAxisY.lastT);
            if (gizmoAxisZ.selected)
                adjust += Vector3(0, 0, 1) * (gizmoAxisZ.t - gizmoAxisZ.lastT);

            // Special handling for uniform scale: use the unmodified X-axis movement only
            if (editMode == EDIT_SCALE && gizmoAxisX.selected && gizmoAxisY.selected && gizmoAxisZ.selected)
            {
                float x = gizmoAxisX.t - gizmoAxisX.lastT;
                adjust = Vector3(x, x, x);
            }

            moved = ScaleNodes(adjust);
        }

        if (moved)
        {
            GizmoMoved();
            UpdateNodeAttributes();
            needGizmoUndo = true;
        }
    }
    else
    {
        if (previousGizmoDrag)
            StoreGizmoEditActions();
    }

    previousGizmoDrag = drag;
}

bool IsGizmoSelected()
{
    return gizmo !is null && gizmo.enabled && (gizmoAxisX.selected || gizmoAxisY.selected || gizmoAxisZ.selected);
}

bool MoveNodes(Vector3 adjust)
{
    bool moved = false;

    if (adjust.length > M_EPSILON)
    {
        for (uint i = 0; i < editNodes.length; ++i)
        {
            if (moveSnap)
            {
                float moveStepScaled = moveStep * snapScale;
                adjust.x = Floor(adjust.x / moveStepScaled + 0.5) * moveStepScaled;
                adjust.y = Floor(adjust.y / moveStepScaled + 0.5) * moveStepScaled;
                adjust.z = Floor(adjust.z / moveStepScaled + 0.5) * moveStepScaled;
            }

            Node@ node = editNodes[i];
            Vector3 nodeAdjust = adjust;
            if (axisMode == AXIS_LOCAL && editNodes.length == 1)
                nodeAdjust = node.worldRotation * nodeAdjust;

            Vector3 worldPos = node.worldPosition;
            Vector3 oldPos = node.position;

            worldPos += nodeAdjust;

            if (node.parent is null)
                node.position = worldPos;
            else
                node.position = node.parent.WorldToLocal(worldPos);

            if (node.position != oldPos)
                moved = true;
        }
    }

    return moved;
}

bool RotateNodes(Vector3 adjust)
{
    bool moved = false;

    if (rotateSnap)
    {
        float rotateStepScaled = rotateStep * snapScale;
        adjust.x = Floor(adjust.x / rotateStepScaled + 0.5) * rotateStepScaled;
        adjust.y = Floor(adjust.y / rotateStepScaled + 0.5) * rotateStepScaled;
        adjust.z = Floor(adjust.z / rotateStepScaled + 0.5) * rotateStepScaled;
    }

    if (adjust.length > M_EPSILON)
    {
        moved = true;

        for (uint i = 0; i < editNodes.length; ++i)
        {
            Node@ node = editNodes[i];
            Quaternion rotQuat(adjust);
            if (axisMode == AXIS_LOCAL && editNodes.length == 1)
                node.rotation = node.rotation * rotQuat;
            else
            {
                Vector3 offset = node.worldPosition - gizmoAxisX.axisRay.origin;
                if (node.parent !is null && node.parent.worldRotation != Quaternion(1, 0, 0, 0))
                    rotQuat = node.parent.worldRotation.Inverse() * rotQuat * node.parent.worldRotation;
                node.rotation = rotQuat * node.rotation;
                Vector3 newPosition = gizmoAxisX.axisRay.origin + rotQuat * offset;
                if (node.parent !is null)
                    newPosition = node.parent.WorldToLocal(newPosition);
                node.position = newPosition;
            }
        }
    }

    return moved;
}

bool ScaleNodes(Vector3 adjust)
{
    bool moved = false;

    if (adjust.length > M_EPSILON)
    {
        for (uint i = 0; i < editNodes.length; ++i)
        {
            Node@ node = editNodes[i];

            Vector3 scale = node.scale;
            Vector3 oldScale = scale;

            if (!scaleSnap)
                scale += adjust;
            else
            {
                float scaleStepScaled = scaleStep * snapScale;
                if (adjust.x != 0)
                {
                    scale.x += adjust.x * scaleStepScaled;
                    scale.x = Floor(scale.x / scaleStepScaled + 0.5) * scaleStepScaled;
                }
                if (adjust.y != 0)
                {
                    scale.y += adjust.y * scaleStepScaled;
                    scale.y = Floor(scale.y / scaleStepScaled + 0.5) * scaleStepScaled;
                }
                if (adjust.z != 0)
                {
                    scale.z += adjust.z * scaleStepScaled;
                    scale.z = Floor(scale.z / scaleStepScaled + 0.5) * scaleStepScaled;
                }
            }

            if (scale != oldScale)
                moved = true;

            node.scale = scale;
        }
    }

    return moved;
}

void StoreGizmoEditActions()
{
    if (needGizmoUndo && editNodes.length > 0 && oldGizmoTransforms.length == editNodes.length)
    {
        EditActionGroup group;

        for (uint i = 0; i < editNodes.length; ++i)
        {
            EditNodeTransformAction action;
            action.Define(editNodes[i], oldGizmoTransforms[i]);
            group.actions.Push(action);
        }

        SaveEditActionGroup(group);
        SetSceneModified();
    }

    needGizmoUndo = false;
}

// Urho3D editor node transform gizmo handling

Node@ gizmoNode;
StaticModel@ gizmo;

const float axisMaxD = 0.1;
const float axisMaxT = 1.0;
const float rotSensitivity = 50.0;

EditMode lastGizmoMode;

// For undo
bool previousGizmoDrag;
bool needGizmoUndo;
Array<Transform> oldGizmoTransforms;

class GizmoAxis
{
    Ray axisRay;
    bool selected;
    bool lastSelected;
    float t;
    float d;
    float lastT;
    float lastD;

    GizmoAxis()
    {
        selected = false;
        lastSelected = false;
        t = 0.0;
        d = 0.0;
        lastT = 0.0;
        lastD = 0.0;
    }

    void Update(Ray cameraRay, float scale, bool drag)
    {
        // Do not select when UI has modal element
        if (ui.HasModalElement())
        {
            selected = false;
            return;
        }

        Vector3 closest = cameraRay.ClosestPoint(axisRay);
        Vector3 projected = axisRay.Project(closest);
        d = axisRay.Distance(closest);
        t = (projected - axisRay.origin).DotProduct(axisRay.direction);

        // Determine the sign of d from a plane that goes through the camera position to the axis
        Plane axisPlane(cameraNode.position, axisRay.origin, axisRay.origin + axisRay.direction);
        if (axisPlane.Distance(closest) < 0.0)
            d = -d;

        // Update selected status only when not dragging
        if (!drag)
        {
            selected = Abs(d) < axisMaxD * scale && t >= -axisMaxD * scale && t <= axisMaxT * scale;
            lastT = t;
            lastD = d;
        }
    }

    void Moved()
    {
        lastT = t;
        lastD = d;
    }
}

GizmoAxis gizmoAxisX;
GizmoAxis gizmoAxisY;
GizmoAxis gizmoAxisZ;

void CreateGizmo()
{
    gizmoNode = Node();
    gizmo = gizmoNode.CreateComponent("StaticModel");
    gizmo.model = cache.GetResource("Model", "Models/Editor/Axes.mdl");
    gizmo.materials[0] = cache.GetResource("Material", "Materials/Editor/RedUnlit.xml");
    gizmo.materials[1] = cache.GetResource("Material", "Materials/Editor/GreenUnlit.xml");
    gizmo.materials[2] = cache.GetResource("Material", "Materials/Editor/BlueUnlit.xml");
    gizmo.enabled = false;
    gizmo.viewMask = 0x80000000; // Editor raycasts use viewmask 0x7fffffff
    gizmo.occludee = false;

    gizmoAxisX.lastSelected = false;
    gizmoAxisY.lastSelected = false;
    gizmoAxisZ.lastSelected = false;
    lastGizmoMode = EDIT_MOVE;
}

void HideGizmo()
{
    if (gizmo !is null)
        gizmo.enabled = false;
}

void ShowGizmo()
{
    if (gizmo !is null)
    {
        gizmo.enabled = true;

        // Because setting enabled = false detaches the gizmo from octree,
        // and it is a manually added drawable, must readd to octree when showing
        if (editorScene.octree !is null)
            editorScene.octree.AddManualDrawable(gizmo);
    }
}

void UpdateGizmo()
{
    UseGizmo();
    PositionGizmo();
    ResizeGizmo();
}

void PositionGizmo()
{
    if (gizmo is null)
        return;

    Vector3 center(0, 0, 0);
    bool containsScene = false;

    for (uint i = 0; i < editNodes.length; ++i)
    {
        // Scene's transform should not be edited, so hide gizmo if it is included
        if (editNodes[i] is editorScene)
        {
            containsScene = true;
            break;
        }
        center += editNodes[i].worldPosition;
    }

    if (editNodes.empty || containsScene)
    {
        HideGizmo();
        return;
    }

    center /= editNodes.length;
    gizmoNode.position = center;

    if (axisMode == AXIS_WORLD || editNodes.length > 1)
        gizmoNode.rotation = Quaternion();
    else
        gizmoNode.rotation = editNodes[0].worldRotation;

    if (editMode != lastGizmoMode)
    {
        switch (editMode)
        {
        case EDIT_MOVE:
            gizmo.model = cache.GetResource("Model", "Models/Editor/Axes.mdl");
            break;

        case EDIT_ROTATE:
            gizmo.model = cache.GetResource("Model", "Models/Editor/RotateAxes.mdl");
            break;

        case EDIT_SCALE:
            gizmo.model = cache.GetResource("Model", "Models/Editor/ScaleAxes.mdl");
            break;
        }

        lastGizmoMode = editMode;
    }

    if ((editMode != EDIT_SELECT && !orbiting) && !gizmo.enabled)
        ShowGizmo();
    else if ((editMode == EDIT_SELECT || orbiting) && gizmo.enabled)
        HideGizmo();
}

void ResizeGizmo()
{
    if (gizmo is null || !gizmo.enabled)
        return;

    float scale = 0.1 / camera.zoom;

    if (camera.orthographic)
        scale *= camera.orthoSize;
    else
        scale *= (camera.view * gizmoNode.position).z;

    gizmoNode.scale = Vector3(scale, scale, scale);
}

void CalculateGizmoAxes()
{
    gizmoAxisX.axisRay = Ray(gizmoNode.position, gizmoNode.rotation * Vector3(1, 0, 0));
    gizmoAxisY.axisRay = Ray(gizmoNode.position, gizmoNode.rotation * Vector3(0, 1, 0));
    gizmoAxisZ.axisRay = Ray(gizmoNode.position, gizmoNode.rotation * Vector3(0, 0, 1));
}

void GizmoMoved()
{
    gizmoAxisX.Moved();
    gizmoAxisY.Moved();
    gizmoAxisZ.Moved();
}

void UseGizmo()
{
    if (gizmo is null || !gizmo.enabled || editMode == EDIT_SELECT)
    {
        StoreGizmoEditActions();
        previousGizmoDrag = false;
        return;
    }

    IntVector2 pos = ui.cursorPosition;
    if (ui.GetElementAt(pos) !is null)
        return;
    Ray cameraRay = GetActiveViewportCameraRay();
    float scale = gizmoNode.scale.x;

    // Recalculate axes only when not left-dragging
    bool drag = input.mouseButtonDown[MOUSEB_LEFT];
    if (!drag)
        CalculateGizmoAxes();

    gizmoAxisX.Update(cameraRay, scale, drag);
    gizmoAxisY.Update(cameraRay, scale, drag);
    gizmoAxisZ.Update(cameraRay, scale, drag);

    if (gizmoAxisX.selected != gizmoAxisX.lastSelected)
    {
        gizmo.materials[0] = cache.GetResource("Material", gizmoAxisX.selected ? "Materials/Editor/BrightRedUnlit.xml" :
            "Materials/Editor/RedUnlit.xml");
        gizmoAxisX.lastSelected = gizmoAxisX.selected;
    }
    if (gizmoAxisY.selected != gizmoAxisY.lastSelected)
    {
        gizmo.materials[1] = cache.GetResource("Material", gizmoAxisY.selected ? "Materials/Editor/BrightGreenUnlit.xml" :
            "Materials/Editor/GreenUnlit.xml");
        gizmoAxisY.lastSelected = gizmoAxisY.selected;
    }
    if (gizmoAxisZ.selected != gizmoAxisZ.lastSelected)
    {
        gizmo.materials[2] = cache.GetResource("Material", gizmoAxisZ.selected ? "Materials/Editor/BrightBlueUnlit.xml" :
            "Materials/Editor/BlueUnlit.xml");
        gizmoAxisZ.lastSelected = gizmoAxisZ.selected;
    };

    if (drag)
    {
        // Store initial transforms for undo when gizmo drag started
        if (!previousGizmoDrag)
        {
            oldGizmoTransforms.Resize(editNodes.length);
            for (uint i = 0; i < editNodes.length; ++i)
                oldGizmoTransforms[i].Define(editNodes[i]);
        }

        bool moved = false;

        if (editMode == EDIT_MOVE)
        {
            Vector3 adjust(0, 0, 0);
            if (gizmoAxisX.selected)
                adjust += Vector3(1, 0, 0) * (gizmoAxisX.t - gizmoAxisX.lastT);
            if (gizmoAxisY.selected)
                adjust += Vector3(0, 1, 0) * (gizmoAxisY.t - gizmoAxisY.lastT);
            if (gizmoAxisZ.selected)
                adjust += Vector3(0, 0, 1) * (gizmoAxisZ.t - gizmoAxisZ.lastT);

            moved = MoveNodes(adjust);
        }
        else if (editMode == EDIT_ROTATE)
        {
            Vector3 adjust(0, 0, 0);
            if (gizmoAxisX.selected)
                adjust.x = (gizmoAxisX.d - gizmoAxisX.lastD) * rotSensitivity / scale;
            if (gizmoAxisY.selected)
                adjust.y = -(gizmoAxisY.d - gizmoAxisY.lastD) * rotSensitivity / scale;
            if (gizmoAxisZ.selected)
                adjust.z = (gizmoAxisZ.d - gizmoAxisZ.lastD) * rotSensitivity / scale;

            moved = RotateNodes(adjust);
        }
        else if (editMode == EDIT_SCALE)
        {
            Vector3 adjust(0, 0, 0);
            if (gizmoAxisX.selected)
                adjust += Vector3(1, 0, 0) * (gizmoAxisX.t - gizmoAxisX.lastT);
            if (gizmoAxisY.selected)
                adjust += Vector3(0, 1, 0) * (gizmoAxisY.t - gizmoAxisY.lastT);
            if (gizmoAxisZ.selected)
                adjust += Vector3(0, 0, 1) * (gizmoAxisZ.t - gizmoAxisZ.lastT);

            // Special handling for uniform scale: use the unmodified X-axis movement only
            if (editMode == EDIT_SCALE && gizmoAxisX.selected && gizmoAxisY.selected && gizmoAxisZ.selected)
            {
                float x = gizmoAxisX.t - gizmoAxisX.lastT;
                adjust = Vector3(x, x, x);
            }

            moved = ScaleNodes(adjust);
        }

        if (moved)
        {
            GizmoMoved();
            UpdateNodeAttributes();
            needGizmoUndo = true;
        }
    }
    else
    {
        if (previousGizmoDrag)
            StoreGizmoEditActions();
    }

    previousGizmoDrag = drag;
}

bool IsGizmoSelected()
{
    return gizmo !is null && gizmo.enabled && (gizmoAxisX.selected || gizmoAxisY.selected || gizmoAxisZ.selected);
}

bool MoveNodes(Vector3 adjust)
{
    bool moved = false;

    if (adjust.length > M_EPSILON)
    {
        for (uint i = 0; i < editNodes.length; ++i)
        {
            if (moveSnap)
            {
                float moveStepScaled = moveStep * snapScale;
                adjust.x = Floor(adjust.x / moveStepScaled + 0.5) * moveStepScaled;
                adjust.y = Floor(adjust.y / moveStepScaled + 0.5) * moveStepScaled;
                adjust.z = Floor(adjust.z / moveStepScaled + 0.5) * moveStepScaled;
            }

            Node@ node = editNodes[i];
            Vector3 nodeAdjust = adjust;
            if (axisMode == AXIS_LOCAL && editNodes.length == 1)
                nodeAdjust = node.worldRotation * nodeAdjust;

            Vector3 worldPos = node.worldPosition;
            Vector3 oldPos = node.position;

            worldPos += nodeAdjust;

            if (node.parent is null)
                node.position = worldPos;
            else
                node.position = node.parent.WorldToLocal(worldPos);

            if (node.position != oldPos)
                moved = true;
        }
    }

    return moved;
}

bool RotateNodes(Vector3 adjust)
{
    bool moved = false;

    if (rotateSnap)
    {
        float rotateStepScaled = rotateStep * snapScale;
        adjust.x = Floor(adjust.x / rotateStepScaled + 0.5) * rotateStepScaled;
        adjust.y = Floor(adjust.y / rotateStepScaled + 0.5) * rotateStepScaled;
        adjust.z = Floor(adjust.z / rotateStepScaled + 0.5) * rotateStepScaled;
    }

    if (adjust.length > M_EPSILON)
    {
        moved = true;

        for (uint i = 0; i < editNodes.length; ++i)
        {
            Node@ node = editNodes[i];
            Quaternion rotQuat(adjust);
            if (axisMode == AXIS_LOCAL && editNodes.length == 1)
                node.rotation = node.rotation * rotQuat;
            else
            {
                Vector3 offset = node.worldPosition - gizmoAxisX.axisRay.origin;
                if (node.parent !is null && node.parent.worldRotation != Quaternion(1, 0, 0, 0))
                    rotQuat = node.parent.worldRotation.Inverse() * rotQuat * node.parent.worldRotation;
                node.rotation = rotQuat * node.rotation;
                Vector3 newPosition = gizmoAxisX.axisRay.origin + rotQuat * offset;
                if (node.parent !is null)
                    newPosition = node.parent.WorldToLocal(newPosition);
                node.position = newPosition;
            }
        }
    }

    return moved;
}

bool ScaleNodes(Vector3 adjust)
{
    bool moved = false;

    if (adjust.length > M_EPSILON)
    {
        for (uint i = 0; i < editNodes.length; ++i)
        {
            Node@ node = editNodes[i];

            Vector3 scale = node.scale;
            Vector3 oldScale = scale;

            if (!scaleSnap)
                scale += adjust;
            else
            {
                float scaleStepScaled = scaleStep * snapScale;
                if (adjust.x != 0)
                {
                    scale.x += adjust.x * scaleStepScaled;
                    scale.x = Floor(scale.x / scaleStepScaled + 0.5) * scaleStepScaled;
                }
                if (adjust.y != 0)
                {
                    scale.y += adjust.y * scaleStepScaled;
                    scale.y = Floor(scale.y / scaleStepScaled + 0.5) * scaleStepScaled;
                }
                if (adjust.z != 0)
                {
                    scale.z += adjust.z * scaleStepScaled;
                    scale.z = Floor(scale.z / scaleStepScaled + 0.5) * scaleStepScaled;
                }
            }

            if (scale != oldScale)
                moved = true;

            node.scale = scale;
        }
    }

    return moved;
}

void StoreGizmoEditActions()
{
    if (needGizmoUndo && editNodes.length > 0 && oldGizmoTransforms.length == editNodes.length)
    {
        EditActionGroup group;

        for (uint i = 0; i < editNodes.length; ++i)
        {
            EditNodeTransformAction action;
            action.Define(editNodes[i], oldGizmoTransforms[i]);
            group.actions.Push(action);
        }

        SaveEditActionGroup(group);
        SetSceneModified();
    }

    needGizmoUndo = false;
}

Fix wrong matrix math in gizmo scaling. Take zoom into account.


/**
 * Copyright 2017 FreshPlanet
 * 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.freshplanet.ane.AirPushNotification {

	import flash.events.EventDispatcher;
	import flash.events.StatusEvent;
	import flash.external.ExtensionContext;
	import flash.system.Capabilities;

    /**
     *
     */
    public class PushNotification extends EventDispatcher {

        // --------------------------------------------------------------------------------------//
        //																						 //
        // 									   PUBLIC API										 //
        // 																						 //
        // --------------------------------------------------------------------------------------//

		public static const RECURRENCE_NONE:int = 0;
		public static const RECURRENCE_DAILY:int = 1;
		public static const RECURRENCE_WEEK:int = 2;
		public static const RECURRENCE_MONTH:int = 3;
		public static const RECURRENCE_YEAR:int = 4;

		public static const DEFAULT_LOCAL_NOTIFICATION_ID:int = 0;

        /**
         * If true, logs will be displayed at the ActionScript level.
         */
        public static var logEnabled:Boolean = false;

        /**
         *
         */
        public static function get isSupported():Boolean {
            return _isIOS() || _isAndroid();
        }

        /**
         *
         */
        public static function get instance():PushNotification {
            return _instance ? _instance : new PushNotification();
        }

		/**
		 *  return true if notifs are enabled for this app in device settings
		 *  If iOS < 8 or android < 4.1 this isn't available, so will always return true.
		 */
		public function getNotificationsEnabled():void {

			if (!isSupported)
				return;

			_context.call("getNotificationsEnabled");
		}

		/**
		 * Check if notificationChannel is enabled in Android notification settings
		 * @param channelId
		 * @return
		 */
		public function isNotificationChannelEnabled(channelId:String):Boolean {

			if (!isSupported)
				return false;

			if(_isIOS())
				return true;

			return _context.call("checkNotificationChannelEnabled", channelId);
		}

		/**
		 * return true if OS permits sending user to settings (iOS 8, Android
		 */
		public function get canSendUserToSettings():Boolean {

			if (!isSupported)
				return false;

			return _context.call("getCanSendUserToSettings");
		}

        /**
         *
         */
		public function openDeviceNotificationSettings(channelId:String = null):void {

			if(!channelId)
				channelId = "";

			if (isSupported)
                _context.call("openDeviceNotificationSettings", channelId);
		}

        /**
		 * Needs Project id for Android Notifications.
		 * The project id is the one the developer used to register to gcm.
		 * @param projectId: project id to use
		 * @param iOSNotificationCategories: notification categories used on iOS (PushNotificationCategoryiOS)
		 */
		public function registerForPushNotification(projectId:String = null, iOSNotificationCategories:Array = null):void {

			if(!isSupported)
				return;

			if (_isAndroid())
                _context.call("registerPush", projectId);
			else if(_isIOS())
				_context.call("registerPush", iOSNotificationCategories != null ? JSON.stringify(iOSNotificationCategories) : null);
		}

        /**
         *
         * @param value
         */
		public function setBadgeNumberValue(value:int):void {

			if (isSupported)
                _context.call("setBadgeNb", value);
		}

		/**
		 * Sends a local notification to the device.
		 * @param message   the local notification text displayed
		 * @param timestamp when the local notification should appear (in sec)
		 * @param title     (Android Only) Title of the local notification
		 * @param recurrenceType
         * @param notificationId
         * @param deepLinkPath
         * @param iconPath
         * @param groupId  used to group notifications together
         * @param categoryId  used to display custom summaries on iOS
         * @param makeSquare  used to display square icons on Android, will default to round
		 */
		public function sendLocalNotification(message:String,
                                              timestamp:int,
                                              title:String,
                                              recurrenceType:int = RECURRENCE_NONE,
											  notificationId:int = DEFAULT_LOCAL_NOTIFICATION_ID,
                                              deepLinkPath:String = null,
                                              iconPath:String = null,
                                              groupId:String = null,
											  categoryId:String = null,
											  makeSquare:Boolean = false
		):void {

			if (!isSupported)
				return;

			if(!iconPath)
				iconPath = "";

			if(!deepLinkPath)
				deepLinkPath = "";

			if(!groupId)
				groupId = "";

			if(!categoryId)
				categoryId = "";

			_context.call("sendLocalNotification", message, timestamp, title, recurrenceType, notificationId, deepLinkPath, iconPath, groupId, categoryId, makeSquare);

		}

		/**
      	* Not implemented on Android for now.
      	* @param notificationId
	  	*/
	    public function cancelLocalNotification(notificationId:int = DEFAULT_LOCAL_NOTIFICATION_ID):void {

	       	if (isSupported) {

	         	if (notificationId == DEFAULT_LOCAL_NOTIFICATION_ID)
                    _context.call("cancelLocalNotification");
	         	else
                    _context.call("cancelLocalNotification", notificationId);
	       	}
	    }

        /**
         *
         */
		public function cancelAllLocalNotifications():void {

			if (isSupported)
                _context.call("cancelAllLocalNotifications");
		}

        /**
         *
         * @param value
         */
		public function setIsAppInForeground(value:Boolean, appGroupId:String):void {

			if (isSupported)
                _context.call("setIsAppInForeground", value, appGroupId);
		}

        /**
         *
         * @param listener
         */
		public function addListenerForStarterNotifications(listener:Function):void {

			if (isSupported) {

				this.addEventListener(PushNotificationEvent.APP_STARTING_FROM_NOTIFICATION_EVENT, listener);
                _context.call("fetchStarterNotification");
			}
		}

		public function checkAppNotificationSettingsRequest():void {

			if (_isIOS()) {
				_context.call("checkAppNotificationSettingsRequest");
			}
		}

        public function storeTrackingNotifUrl(url:String, appGroupId:String):void {

            if (isSupported)
                _context.call("storeNotifTrackingInfo", url, appGroupId);
        }

		/**
		 * Create Android notification channel
		 * @param channelId
		 * @param channelName
		 * @param importance
		 * @param enableLights
		 * @param enableVibration
		 */
		public function createAndroidNotificationChannel(channelId:String, channelName:String, importance:int, enableLights:Boolean, enableVibration:Boolean):void {

			if (_isAndroid())
				_context.call("createNotificationChannel", channelId, channelName, importance, enableLights, enableVibration);
		}

		// --------------------------------------------------------------------------------------//
        //																						 //
        // 									 	PRIVATE API										 //
        // 																						 //
        // --------------------------------------------------------------------------------------//

        private static const EXTENSION_ID:String = "com.freshplanet.ane.AirPushNotification";
        private static var _instance:PushNotification = null;
        private var _context:ExtensionContext = null;

        /**
         * "private" singleton constructor
         */
        public function PushNotification() {

            super();

            if (_instance)
                throw Error("This is a singleton, use getInstance(), do not call the constructor directly.");

            _instance = this;

            _context = ExtensionContext.createExtensionContext(EXTENSION_ID, null);

            if (!_context)
                _log("ERROR", "Extension context is null. Please check if extension.xml is setup correctly.");
            else
                _context.addEventListener(StatusEvent.STATUS, _onStatus);
        }

        /**
         *
         * @param strings
         */
        private function _log(...strings):void {

            if (logEnabled) {

                strings.unshift(EXTENSION_ID);
                trace.apply(null, strings);
            }
        }

        /**
         *
         * @return  true if iOS
         */
        private static function _isIOS():Boolean {
			return Capabilities.manufacturer.indexOf("iOS") > -1 && Capabilities.os.indexOf("x86_64") < 0 && Capabilities.os.indexOf("i386") < 0;
        }

        /**
         *
         * @return  true if Android
         */
        private static function _isAndroid():Boolean {
            return Capabilities.manufacturer.indexOf("Android") > -1;
        }

        /**
         *
         * @param e
         */
        private function _onStatus(e:StatusEvent):void {

            var event:PushNotificationEvent = null;
            var data:String = e.level;

            switch (e.code) {

                case "TOKEN_SUCCESS":
                    event = new PushNotificationEvent(PushNotificationEvent.PERMISSION_GIVEN_WITH_TOKEN_EVENT);
                    event.token = e.level;
                    break;

                case "TOKEN_FAIL":
                    event = new PushNotificationEvent(PushNotificationEvent.PERMISSION_REFUSED_EVENT);
                    event.errorCode = "NativeCodeError";
                    event.errorMessage = e.level;
                    break;

                case "COMING_FROM_NOTIFICATION":
                    event = new PushNotificationEvent(PushNotificationEvent.COMING_FROM_NOTIFICATION_EVENT);

                    if (data != null) {

                        try {
                            event.parameters = JSON.parse(data);
                        }
                        catch (error:Error) {
                            trace("[PushNotification Error]", "cannot parse the params string", data);
                        }
                    }
                    break;

                case "APP_STARTING_FROM_NOTIFICATION":

                    event = new PushNotificationEvent(PushNotificationEvent.APP_STARTING_FROM_NOTIFICATION_EVENT);

                    if (data != null) {

                        try {
                            event.parameters = JSON.parse(data);
                        }
                        catch (error:Error) {
                            trace("[PushNotification Error]", "cannot parse the params string", data);
                        }
                    }
                    break;

                case "APP_BROUGHT_TO_FOREGROUND_FROM_NOTIFICATION":

                    event = new PushNotificationEvent(PushNotificationEvent.APP_BROUGHT_TO_FOREGROUND_FROM_NOTIFICATION_EVENT);

                    if (data != null) {

                        try {
                            event.parameters = JSON.parse(data);
                        }
                        catch (error:Error) {
                            trace("[PushNotification Error]", "cannot parse the params string", data);
                        }
                    }
                    break;

                case "APP_STARTED_IN_BACKGROUND_FROM_NOTIFICATION":

                    event = new PushNotificationEvent(PushNotificationEvent.APP_STARTED_IN_BACKGROUND_FROM_NOTIFICATION_EVENT);

                    if (data != null) {

                        try {
                            event.parameters = JSON.parse(data);
                        }
                        catch (error:Error) {
                            trace("[PushNotification Error]", "cannot parse the params string", data);
                        }
                    }
                    break;

                case "NOTIFICATION_RECEIVED_WHEN_IN_FOREGROUND":

                    event = new PushNotificationEvent(PushNotificationEvent.NOTIFICATION_RECEIVED_WHEN_IN_FOREGROUND_EVENT);

                    if (data != null) {

                        try {
                            event.parameters = JSON.parse(data);
                        }
                        catch (error:Error) {
                            trace("[PushNotification Error]", "cannot parse the params string", data);
                        }
                    }
                    break;

                case "NOTIFICATION_SETTINGS_ENABLED":
                    event = new PushNotificationEvent(PushNotificationEvent.NOTIFICATION_SETTINGS_ENABLED);
                    break;

                case "NOTIFICATION_SETTINGS_DISABLED":
                    event = new PushNotificationEvent(PushNotificationEvent.NOTIFICATION_SETTINGS_DISABLED);
                    break;

				case "GET_NOTIFICATIONS_ENABLED_RESULT":
					event = new PushNotificationEvent(PushNotificationEvent.GET_NOTIFICATIONS_ENABLED_RESULT, e.level == "true");
					break;

				case "OPEN_APP_NOTIFICATION_SETTINGS":
					event = new PushNotificationEvent(PushNotificationEvent.OPEN_APP_NOTIFICATION_SETTINGS);
					break;

                case "LOGGING":
                    trace(e, e.level);
                    break;
            }

            if (event != null)
                this.dispatchEvent(event);
        }

	}
}

/**
 * Copyright 2017 FreshPlanet
 * 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.freshplanet.ane.AirPushNotification {

	import flash.events.EventDispatcher;
	import flash.events.StatusEvent;
	import flash.external.ExtensionContext;
	import flash.system.Capabilities;

    /**
     *
     */
    public class PushNotification extends EventDispatcher {

        // --------------------------------------------------------------------------------------//
        //																						 //
        // 									   PUBLIC API										 //
        // 																						 //
        // --------------------------------------------------------------------------------------//

		public static const RECURRENCE_NONE:int = 0;
		public static const RECURRENCE_DAILY:int = 1;
		public static const RECURRENCE_WEEK:int = 2;
		public static const RECURRENCE_MONTH:int = 3;
		public static const RECURRENCE_YEAR:int = 4;

		public static const DEFAULT_LOCAL_NOTIFICATION_ID:int = 0;

        /**
         * If true, logs will be displayed at the ActionScript level.
         */
        public static var logEnabled:Boolean = false;

        /**
         *
         */
        public static function get isSupported():Boolean {
            return _isIOS() || _isAndroid();
        }

        /**
         *
         */
        public static function get instance():PushNotification {
            return _instance ? _instance : new PushNotification();
        }

        private function callContext(...args):*
        {
            var context: ExtensionContext = getContext();
            if(!context) {
                _log("ERROR", "Extension context is null. Please check if extension.xml is setup correctly.")
                return null;
            }
            try {
                return context.call.apply(context, args);
            } catch (e: Error) {
                _log("ERROR", "Error calling extension context: " + e.message + " : " + e.getStackTrace());
            }
            return null;
        }

		/**
		 *  return true if notifs are enabled for this app in device settings
		 *  If iOS < 8 or android < 4.1 this isn't available, so will always return true.
		 */
		public function getNotificationsEnabled():void {

			if (!isSupported)
				return;

			callContext("getNotificationsEnabled");
		}

		/**
		 * Check if notificationChannel is enabled in Android notification settings
		 * @param channelId
		 * @return
		 */
		public function isNotificationChannelEnabled(channelId:String):Boolean {

			if (!isSupported)
				return false;

			if(_isIOS())
				return true;

			return callContext("checkNotificationChannelEnabled", channelId);
		}

		/**
		 * return true if OS permits sending user to settings (iOS 8, Android
		 */
		public function get canSendUserToSettings():Boolean {

			if (!isSupported)
				return false;

			return callContext("getCanSendUserToSettings");
		}

        /**
         *
         */
		public function openDeviceNotificationSettings(channelId:String = null):void {

			if(!channelId)
				channelId = "";

			if (isSupported)
                callContext("openDeviceNotificationSettings", channelId);
		}

        /**
		 * Needs Project id for Android Notifications.
		 * The project id is the one the developer used to register to gcm.
		 * @param projectId: project id to use
		 * @param iOSNotificationCategories: notification categories used on iOS (PushNotificationCategoryiOS)
		 */
		public function registerForPushNotification(projectId:String = null, iOSNotificationCategories:Array = null):void {

			if(!isSupported)
				return;

			if (_isAndroid())
                callContext("registerPush", projectId);
			else if(_isIOS())
				callContext("registerPush", iOSNotificationCategories != null ? JSON.stringify(iOSNotificationCategories) : null);
		}

        /**
         *
         * @param value
         */
		public function setBadgeNumberValue(value:int):void {

			if (isSupported)
                callContext("setBadgeNb", value);
		}

		/**
		 * Sends a local notification to the device.
		 * @param message   the local notification text displayed
		 * @param timestamp when the local notification should appear (in sec)
		 * @param title     (Android Only) Title of the local notification
		 * @param recurrenceType
         * @param notificationId
         * @param deepLinkPath
         * @param iconPath
         * @param groupId  used to group notifications together
         * @param categoryId  used to display custom summaries on iOS
         * @param makeSquare  used to display square icons on Android, will default to round
		 */
		public function sendLocalNotification(message:String,
                                              timestamp:int,
                                              title:String,
                                              recurrenceType:int = RECURRENCE_NONE,
											  notificationId:int = DEFAULT_LOCAL_NOTIFICATION_ID,
                                              deepLinkPath:String = null,
                                              iconPath:String = null,
                                              groupId:String = null,
											  categoryId:String = null,
											  makeSquare:Boolean = false
		):void {

			if (!isSupported)
				return;

			if(!iconPath)
				iconPath = "";

			if(!deepLinkPath)
				deepLinkPath = "";

			if(!groupId)
				groupId = "";

			if(!categoryId)
				categoryId = "";

			callContext("sendLocalNotification", message, timestamp, title, recurrenceType, notificationId, deepLinkPath, iconPath, groupId, categoryId, makeSquare);

		}

		/**
      	* Not implemented on Android for now.
      	* @param notificationId
	  	*/
	    public function cancelLocalNotification(notificationId:int = DEFAULT_LOCAL_NOTIFICATION_ID):void {

	       	if (isSupported) {

	         	if (notificationId == DEFAULT_LOCAL_NOTIFICATION_ID)
                    callContext("cancelLocalNotification");
	         	else
                    callContext("cancelLocalNotification", notificationId);
	       	}
	    }

        /**
         *
         */
		public function cancelAllLocalNotifications():void {

			if (isSupported)
                callContext("cancelAllLocalNotifications");
		}

        /**
         *
         * @param value
         */
		public function setIsAppInForeground(value:Boolean, appGroupId:String):void {

			if (isSupported)
                callContext("setIsAppInForeground", value, appGroupId);
		}

        /**
         *
         * @param listener
         */
		public function addListenerForStarterNotifications(listener:Function):void {

			if (isSupported) {

				this.addEventListener(PushNotificationEvent.APP_STARTING_FROM_NOTIFICATION_EVENT, listener);
                callContext("fetchStarterNotification");
			}
		}

		public function checkAppNotificationSettingsRequest():void {

			if (_isIOS()) {
				callContext("checkAppNotificationSettingsRequest");
			}
		}

        public function storeTrackingNotifUrl(url:String, appGroupId:String):void {

            if (isSupported)
                callContext("storeNotifTrackingInfo", url, appGroupId);
        }

		/**
		 * Create Android notification channel
		 * @param channelId
		 * @param channelName
		 * @param importance
		 * @param enableLights
		 * @param enableVibration
		 */
		public function createAndroidNotificationChannel(channelId:String, channelName:String, importance:int, enableLights:Boolean, enableVibration:Boolean):void {

			if (_isAndroid())
				callContext("createNotificationChannel", channelId, channelName, importance, enableLights, enableVibration);
		}

		// --------------------------------------------------------------------------------------//
        //																						 //
        // 									 	PRIVATE API										 //
        // 																						 //
        // --------------------------------------------------------------------------------------//

        private static const EXTENSION_ID:String = "com.freshplanet.ane.AirPushNotification";
        private static var _instance:PushNotification = null;

        /**
         * "private" singleton constructor
         */
        public function PushNotification() {

            super();

            if (_instance)
                throw Error("This is a singleton, use getInstance(), do not call the constructor directly.");

            _instance = this;
        }

        private var __context:ExtensionContext;
        private function getContext():ExtensionContext
        {
            if (!__context) {
                __context = ExtensionContext.createExtensionContext(EXTENSION_ID, null);
                if (!__context) {
                    _log("ERROR", "Extension context is null. Please check if extension.xml is setup correctly.");
                } else {
                    __context.addEventListener(StatusEvent.STATUS, _onStatus);
                }
            }
            return __context;
        }

        /**
         *
         * @param strings
         */
        private function _log(...strings):void {

            if (logEnabled) {

                strings.unshift(EXTENSION_ID);
                trace.apply(null, strings);
            }
        }

        /**
         *
         * @return  true if iOS
         */
        private static function _isIOS():Boolean {
			return Capabilities.manufacturer.indexOf("iOS") > -1 && Capabilities.os.indexOf("x86_64") < 0 && Capabilities.os.indexOf("i386") < 0;
        }

        /**
         *
         * @return  true if Android
         */
        private static function _isAndroid():Boolean {
            return Capabilities.manufacturer.indexOf("Android") > -1;
        }

        /**
         *
         * @param e
         */
        private function _onStatus(e:StatusEvent):void {

            var event:PushNotificationEvent = null;
            var data:String = e.level;

            switch (e.code) {

                case "TOKEN_SUCCESS":
                    event = new PushNotificationEvent(PushNotificationEvent.PERMISSION_GIVEN_WITH_TOKEN_EVENT);
                    event.token = e.level;
                    break;

                case "TOKEN_FAIL":
                    event = new PushNotificationEvent(PushNotificationEvent.PERMISSION_REFUSED_EVENT);
                    event.errorCode = "NativeCodeError";
                    event.errorMessage = e.level;
                    break;

                case "COMING_FROM_NOTIFICATION":
                    event = new PushNotificationEvent(PushNotificationEvent.COMING_FROM_NOTIFICATION_EVENT);

                    if (data != null) {

                        try {
                            event.parameters = JSON.parse(data);
                        }
                        catch (error:Error) {
                            trace("[PushNotification Error]", "cannot parse the params string", data);
                        }
                    }
                    break;

                case "APP_STARTING_FROM_NOTIFICATION":

                    event = new PushNotificationEvent(PushNotificationEvent.APP_STARTING_FROM_NOTIFICATION_EVENT);

                    if (data != null) {

                        try {
                            event.parameters = JSON.parse(data);
                        }
                        catch (error:Error) {
                            trace("[PushNotification Error]", "cannot parse the params string", data);
                        }
                    }
                    break;

                case "APP_BROUGHT_TO_FOREGROUND_FROM_NOTIFICATION":

                    event = new PushNotificationEvent(PushNotificationEvent.APP_BROUGHT_TO_FOREGROUND_FROM_NOTIFICATION_EVENT);

                    if (data != null) {

                        try {
                            event.parameters = JSON.parse(data);
                        }
                        catch (error:Error) {
                            trace("[PushNotification Error]", "cannot parse the params string", data);
                        }
                    }
                    break;

                case "APP_STARTED_IN_BACKGROUND_FROM_NOTIFICATION":

                    event = new PushNotificationEvent(PushNotificationEvent.APP_STARTED_IN_BACKGROUND_FROM_NOTIFICATION_EVENT);

                    if (data != null) {

                        try {
                            event.parameters = JSON.parse(data);
                        }
                        catch (error:Error) {
                            trace("[PushNotification Error]", "cannot parse the params string", data);
                        }
                    }
                    break;

                case "NOTIFICATION_RECEIVED_WHEN_IN_FOREGROUND":

                    event = new PushNotificationEvent(PushNotificationEvent.NOTIFICATION_RECEIVED_WHEN_IN_FOREGROUND_EVENT);

                    if (data != null) {

                        try {
                            event.parameters = JSON.parse(data);
                        }
                        catch (error:Error) {
                            trace("[PushNotification Error]", "cannot parse the params string", data);
                        }
                    }
                    break;

                case "NOTIFICATION_SETTINGS_ENABLED":
                    event = new PushNotificationEvent(PushNotificationEvent.NOTIFICATION_SETTINGS_ENABLED);
                    break;

                case "NOTIFICATION_SETTINGS_DISABLED":
                    event = new PushNotificationEvent(PushNotificationEvent.NOTIFICATION_SETTINGS_DISABLED);
                    break;

				case "GET_NOTIFICATIONS_ENABLED_RESULT":
					event = new PushNotificationEvent(PushNotificationEvent.GET_NOTIFICATIONS_ENABLED_RESULT, e.level == "true");
					break;

				case "OPEN_APP_NOTIFICATION_SETTINGS":
					event = new PushNotificationEvent(PushNotificationEvent.OPEN_APP_NOTIFICATION_SETTINGS);
					break;

                case "LOGGING":
                    trace(e, e.level);
                    break;
            }

            if (event != null)
                this.dispatchEvent(event);
        }

	}
}

create context lazily to avoid intermittent #3500 errors


package aerys.minko.render.shader
{
	import aerys.minko.ns.minko_render;
	import aerys.minko.ns.minko_shader;
	import aerys.minko.render.RenderTarget;
	import aerys.minko.render.resource.Context3DResource;
	import aerys.minko.render.resource.Program3DResource;
	import aerys.minko.render.shader.compiler.graph.ShaderGraph;
	import aerys.minko.render.shader.compiler.graph.nodes.AbstractNode;
	import aerys.minko.render.shader.part.ShaderPart;
	import aerys.minko.type.Signal;
	import aerys.minko.type.binding.DataBindings;

	import flash.utils.getQualifiedClassName;

	use namespace minko_shader;

	/**
	 * The base class to extend in order to create ActionScript shaders
	 * and program the GPU using AS3.
	 *
	 * @author Jean-Marc Le Roux
	 * @author Romain Gilliotte
	 *
	 * @see aerys.minko.render.shader.ShaderPart
	 * @see aerys.minko.render.shader.ShaderInstance
	 * @see aerys.minko.render.shader.ShaderSignature
	 * @see aerys.minko.render.shader.ShaderDataBindings
	 */
	public class Shader extends ShaderPart
	{
		use namespace minko_shader;
		use namespace minko_render;

		minko_shader var _meshBindings		: ShaderDataBindings			= null;
		minko_shader var _sceneBindings		: ShaderDataBindings			= null;
		minko_shader var _kills				: Vector.<AbstractNode>			= new <AbstractNode>[];

		private var _name					: String						= null;
		private var _enabled				: Boolean						= true;
		private var _defaultSettings		: ShaderSettings				= new ShaderSettings(null);

		private var _instances				: Vector.<ShaderInstance>		= new <ShaderInstance>[]
		private var _numActiveInstances		: uint							= 0;
		private var _numRenderedInstances	: uint							= 0;
		private var _settings				: Vector.<ShaderSettings>		= new <ShaderSettings>[];
		private var _programs				: Vector.<Program3DResource>	= new <Program3DResource>[];

		private var _begin					: Signal						= new Signal('Shader.begin');
		private var _end					: Signal						= new Signal('Shader.end');

		/**
		 * The name of the shader. Default value is the qualified name of the
		 * ActionScript shader class (example: "aerys.minko.render.effect.basic::BasicShader").
		 *
		 * @return
		 *
		 */
		public function get name() : String
		{
			return _name;
		}
		public function set name(value : String) : void
		{
			_name = value;
		}

		/**
		 * The signal executed when the shader (one of its forks) is used
		 * for rendering for the very first time during the current frame.
		 * Callbacks for this signal must accept the following arguments:
		 *

		 *
shader : Shader, the shader executing the signal

		 *
context : Context3D, the context used for rendering

		 *
backBuffer : RenderTarget, the render target used as the back-buffer

		 *

		 *
		 * @return
		 *
		 */
		public function get begin() : Signal
		{
			return _begin;
		}

		/**
		 * The signal executed when the shader (one of its forks) is used
		 * for rendering for the very last time during the current frame.
		 * Callbacks for this signal must accept the following arguments:
		 *

		 *
shader : Shader, the shader executing the signal

		 *
context : Context3D, the context used for rendering

		 *
backBuffer : RenderTarget, the render target used as the back-buffer

		 *

		 *
		 * @return
		 *
		 */
		public function get end() : Signal
		{
			return _end;
		}

		/**
		 * Whether the shader (and all its forks) are enabled for rendering
		 * or not.
		 *
		 * @return
		 *
		 */
		public function get enabled() : Boolean
		{
			return _enabled;
		}
		public function set enabled(value : Boolean) : void
		{
			_enabled = value;
		}

		/**
		 *
		 * @param priority Default value is 0.
		 * @param renderTarget Default value is null.
		 *
		 */
		public function Shader(renderTarget	: RenderTarget	= null,
							   priority		: Number		= 0.0)
		{
			super(this);

			_defaultSettings.renderTarget = renderTarget;
			_defaultSettings.priority = priority;

			_name = getQualifiedClassName(this);
		}

		public function fork(meshBindings	: DataBindings,
							 sceneBindings	: DataBindings) : ShaderInstance
		{
			var pass : ShaderInstance = findPass(meshBindings, sceneBindings);

			if (pass == null)
			{
				var signature	: Signature			= new Signature(_name);
				var config		: ShaderSettings	= findOrCreateSettings(meshBindings, sceneBindings);

				signature.mergeWith(config.signature);

				var program		: Program3DResource	= null;

				if (config.enabled)
				{
					program = findOrCreateProgram(meshBindings, sceneBindings);
					signature.mergeWith(program.signature);
				}

				pass = new ShaderInstance(this, config, program, signature);
				pass.retained.add(shaderInstanceRetainedHandler);
				pass.released.add(shaderInstanceReleasedHandler);

				_instances.push(pass);
			}

			return pass;
		}

		public function disposeUnusedResources() : void
		{
			var numInstances	: uint = _instances.length;
			var currentId		: uint = 0;

			for (var instanceId : uint = 0; instanceId < numInstances; ++instanceId)
			{
				var passInstance : ShaderInstance = _instances[instanceId];

				if (!passInstance.isDisposable)
					_instances[currentId++] = passInstance;
			}

			_instances.length = currentId;
		}

		/**
		 * Override this method to initialize the settings
		 * - such as the blending operands or the triangle culling - of the shader.
		 * This values can be read from both the mesh and the scene bindings.
		 *
		 * @param settings
		 *
		 * @see aerys.minko.render.shader.ShaderSettings
		 *
		 */
		protected function initializeSettings(settings : ShaderSettings) : void
		{
			// nothing
		}

		/**
		 * The getVertexPosition() method is called to evaluate the vertex shader
		 * program that shall be executed on the GPU.
		 *
		 * @return The position of the vertex in clip space (normalized screen space).
		 *
		 */
		protected function getVertexPosition() : SFloat
		{
			throw new Error("The method 'getVertexPosition' must be implemented.");
		}

		/**
		 * The getPixelColor() method is called to evaluate the fragment shader
		 * program that shall be executed on the GPU.
		 *
		 * @return The color of the pixel on the screen.
		 *
		 */
		protected function getPixelColor() : SFloat
		{
			throw new Error("The method 'getPixelColor' must be implemented.");
		}

		private function findPass(meshBindings	: DataBindings,
								  sceneBindings	: DataBindings) : ShaderInstance
		{
			var numPasses : int = _instances.length;

			for (var passId : uint = 0; passId < numPasses; ++passId)
				if (_instances[passId].signature.isValid(meshBindings, sceneBindings))
					return _instances[passId];

			return null;
		}

		private function findOrCreateProgram(meshBindings	: DataBindings,
											 sceneBindings	: DataBindings) : Program3DResource
		{
			var numPrograms	: int = _programs.length;
			var program 	: Program3DResource;

			for (var programId : uint = 0; programId < numPrograms; ++programId)
				if (_programs[programId].signature.isValid(meshBindings, sceneBindings))
					return _programs[programId];

			var signature		: Signature		= new Signature(_name);

			_meshBindings	= new ShaderDataBindings(meshBindings, signature, Signature.SOURCE_MESH);
			_sceneBindings	= new ShaderDataBindings(sceneBindings, signature, Signature.SOURCE_SCENE);

			var vertexPosition	: AbstractNode	= getVertexPosition()._node;
			var pixelColor		: AbstractNode	= getPixelColor()._node;
			var shaderGraph		: ShaderGraph	= new ShaderGraph(vertexPosition, pixelColor, _kills);

			program			= shaderGraph.generateProgram(_name, signature);
			_meshBindings	= null;
			_sceneBindings	= null;
			_kills.length	= 0;

			_programs.push(program);

			return program;
		}

		private function findOrCreateSettings(meshBindings 	: DataBindings,
											 sceneBindings	: DataBindings) : ShaderSettings
		{
			var numConfigs	: int 				= _settings.length;
			var config		: ShaderSettings	= null;

			for (var configId : int = 0; configId < numConfigs; ++configId)
				if (_settings[configId].signature.isValid(meshBindings, sceneBindings))
					return _settings[configId];

			var signature : Signature = new Signature(_name);

			config			= _defaultSettings.clone(signature);
			_meshBindings	= new ShaderDataBindings(meshBindings, signature, Signature.SOURCE_MESH);
			_sceneBindings	= new ShaderDataBindings(sceneBindings, signature, Signature.SOURCE_SCENE);

			initializeSettings(config);

			_meshBindings	= null;
			_sceneBindings	= null;

			_settings.push(config);

			return config;
		}

		private function shaderInstanceRetainedHandler(instance : ShaderInstance,
													   numUses	: uint) : void
		{
			if (numUses == 1)
			{
				++_numActiveInstances;

				instance.begin.add(shaderInstanceBeginHandler);
				instance.end.add(shaderInstanceEndHandler);
			}
		}

		private function shaderInstanceReleasedHandler(instance : ShaderInstance,
													   numUses	: uint) : void
		{
			if (numUses == 0)
			{
				--_numActiveInstances;

				instance.begin.remove(shaderInstanceBeginHandler);
				instance.end.remove(shaderInstanceEndHandler);
			}
		}

		private function shaderInstanceBeginHandler(instance 	: ShaderInstance,
													context		: Context3DResource,
													backBuffer	: RenderTarget) : void
		{
			if (_numRenderedInstances == 0)
				_begin.execute(this, context, backBuffer);
		}

		private function shaderInstanceEndHandler(instance		: ShaderInstance,
												  context		: Context3DResource,
												  backBuffer	: RenderTarget) : void
		{
			_numRenderedInstances++;

			if (_numRenderedInstances == _numActiveInstances)
			{
				_numRenderedInstances = 0;
				_end.execute(this, context, backBuffer);
			}
		}
	}
}

package aerys.minko.render.shader
{
	import aerys.minko.ns.minko_render;
	import aerys.minko.ns.minko_shader;
	import aerys.minko.render.RenderTarget;
	import aerys.minko.render.resource.Context3DResource;
	import aerys.minko.render.resource.Program3DResource;
	import aerys.minko.render.shader.compiler.graph.ShaderGraph;
	import aerys.minko.render.shader.compiler.graph.nodes.AbstractNode;
	import aerys.minko.render.shader.part.ShaderPart;
	import aerys.minko.type.Signal;
	import aerys.minko.type.binding.DataBindings;

	import flash.utils.getQualifiedClassName;

	use namespace minko_shader;

	/**
	 * The base class to extend in order to create ActionScript shaders
	 * and program the GPU using AS3.
	 *
	 * @author Jean-Marc Le Roux
	 * @author Romain Gilliotte
	 *
	 * @see aerys.minko.render.shader.ShaderPart
	 * @see aerys.minko.render.shader.ShaderInstance
	 * @see aerys.minko.render.shader.ShaderSignature
	 * @see aerys.minko.render.shader.ShaderDataBindings
	 */
	public class Shader extends ShaderPart
	{
		use namespace minko_shader;
		use namespace minko_render;

		minko_shader var _meshBindings		: ShaderDataBindings			= null;
		minko_shader var _sceneBindings		: ShaderDataBindings			= null;
		minko_shader var _kills				: Vector.<AbstractNode>			= new <AbstractNode>[];

		private var _name					: String						= null;
		private var _enabled				: Boolean						= true;
		private var _defaultSettings		: ShaderSettings				= new ShaderSettings(null);

		private var _instances				: Vector.<ShaderInstance>		= new <ShaderInstance>[];
		private var _numActiveInstances		: uint							= 0;
		private var _numRenderedInstances	: uint							= 0;
		private var _settings				: Vector.<ShaderSettings>		= new <ShaderSettings>[];
		private var _programs				: Vector.<Program3DResource>	= new <Program3DResource>[];

		private var _begin					: Signal						= new Signal('Shader.begin');
		private var _end					: Signal						= new Signal('Shader.end');

		/**
		 * The name of the shader. Default value is the qualified name of the
		 * ActionScript shader class (example: "aerys.minko.render.effect.basic::BasicShader").
		 *
		 * @return
		 *
		 */
		public function get name() : String
		{
			return _name;
		}
		public function set name(value : String) : void
		{
			_name = value;
		}

		/**
		 * The signal executed when the shader (one of its forks) is used
		 * for rendering for the very first time during the current frame.
		 * Callbacks for this signal must accept the following arguments:
		 *

		 *
shader : Shader, the shader executing the signal

		 *
context : Context3D, the context used for rendering

		 *
backBuffer : RenderTarget, the render target used as the back-buffer

		 *

		 *
		 * @return
		 *
		 */
		public function get begin() : Signal
		{
			return _begin;
		}

		/**
		 * The signal executed when the shader (one of its forks) is used
		 * for rendering for the very last time during the current frame.
		 * Callbacks for this signal must accept the following arguments:
		 *

		 *
shader : Shader, the shader executing the signal

		 *
context : Context3D, the context used for rendering

		 *
backBuffer : RenderTarget, the render target used as the back-buffer

		 *

		 *
		 * @return
		 *
		 */
		public function get end() : Signal
		{
			return _end;
		}

		/**
		 * Whether the shader (and all its forks) are enabled for rendering
		 * or not.
		 *
		 * @return
		 *
		 */
		public function get enabled() : Boolean
		{
			return _enabled;
		}
		public function set enabled(value : Boolean) : void
		{
			_enabled = value;
		}

		/**
		 *
		 * @param priority Default value is 0.
		 * @param renderTarget Default value is null.
		 *
		 */
		public function Shader(renderTarget	: RenderTarget	= null,
							   priority		: Number		= 0.0)
		{
			super(this);

			_defaultSettings.renderTarget = renderTarget;
			_defaultSettings.priority = priority;

			_name = getQualifiedClassName(this);
		}

		public function fork(meshBindings	: DataBindings,
							 sceneBindings	: DataBindings) : ShaderInstance
		{
			var pass : ShaderInstance = findPass(meshBindings, sceneBindings);

			if (pass == null)
			{
				var signature	: Signature			= new Signature(_name);
				var config		: ShaderSettings	= findOrCreateSettings(meshBindings, sceneBindings);

				signature.mergeWith(config.signature);

				var program		: Program3DResource	= null;

				if (config.enabled)
				{
					program = findOrCreateProgram(meshBindings, sceneBindings);
					signature.mergeWith(program.signature);
				}

				pass = new ShaderInstance(this, config, program, signature);
				pass.retained.add(shaderInstanceRetainedHandler);
				pass.released.add(shaderInstanceReleasedHandler);

				_instances.push(pass);
			}

			return pass;
		}

		public function disposeUnusedResources() : void
		{
			var numInstances	: uint = _instances.length;
			var currentId		: uint = 0;

			for (var instanceId : uint = 0; instanceId < numInstances; ++instanceId)
			{
				var passInstance : ShaderInstance = _instances[instanceId];

				if (!passInstance.isDisposable)
					_instances[currentId++] = passInstance;
			}

			_instances.length = currentId;
		}

		/**
		 * Override this method to initialize the settings
		 * - such as the blending operands or the triangle culling - of the shader.
		 * This values can be read from both the mesh and the scene bindings.
		 *
		 * @param settings
		 *
		 * @see aerys.minko.render.shader.ShaderSettings
		 *
		 */
		protected function initializeSettings(settings : ShaderSettings) : void
		{
			// nothing
		}

		/**
		 * The getVertexPosition() method is called to evaluate the vertex shader
		 * program that shall be executed on the GPU.
		 *
		 * @return The position of the vertex in clip space (normalized screen space).
		 *
		 */
		protected function getVertexPosition() : SFloat
		{
			throw new Error("The method 'getVertexPosition' must be implemented.");
		}

		/**
		 * The getPixelColor() method is called to evaluate the fragment shader
		 * program that shall be executed on the GPU.
		 *
		 * @return The color of the pixel on the screen.
		 *
		 */
		protected function getPixelColor() : SFloat
		{
			throw new Error("The method 'getPixelColor' must be implemented.");
		}

		private function findPass(meshBindings	: DataBindings,
								  sceneBindings	: DataBindings) : ShaderInstance
		{
			var numPasses : int = _instances.length;

			for (var passId : uint = 0; passId < numPasses; ++passId)
				if (_instances[passId].signature.isValid(meshBindings, sceneBindings))
					return _instances[passId];

			return null;
		}

		private function findOrCreateProgram(meshBindings	: DataBindings,
											 sceneBindings	: DataBindings) : Program3DResource
		{
			var numPrograms	: int = _programs.length;
			var program 	: Program3DResource;

			for (var programId : uint = 0; programId < numPrograms; ++programId)
				if (_programs[programId].signature.isValid(meshBindings, sceneBindings))
					return _programs[programId];

			var signature		: Signature		= new Signature(_name);

			_meshBindings	= new ShaderDataBindings(meshBindings, signature, Signature.SOURCE_MESH);
			_sceneBindings	= new ShaderDataBindings(sceneBindings, signature, Signature.SOURCE_SCENE);

			var vertexPosition	: AbstractNode	= getVertexPosition()._node;
			var pixelColor		: AbstractNode	= getPixelColor()._node;
			var shaderGraph		: ShaderGraph	= new ShaderGraph(vertexPosition, pixelColor, _kills);

			program			= shaderGraph.generateProgram(_name, signature);
			_meshBindings	= null;
			_sceneBindings	= null;
			_kills.length	= 0;

			_programs.push(program);

			return program;
		}

		private function findOrCreateSettings(meshBindings 	: DataBindings,
											 sceneBindings	: DataBindings) : ShaderSettings
		{
			var numConfigs	: int 				= _settings.length;
			var config		: ShaderSettings	= null;

			for (var configId : int = 0; configId < numConfigs; ++configId)
				if (_settings[configId].signature.isValid(meshBindings, sceneBindings))
					return _settings[configId];

			var signature : Signature = new Signature(_name);

			config			= _defaultSettings.clone(signature);
			_meshBindings	= new ShaderDataBindings(meshBindings, signature, Signature.SOURCE_MESH);
			_sceneBindings	= new ShaderDataBindings(sceneBindings, signature, Signature.SOURCE_SCENE);

			initializeSettings(config);

			_meshBindings	= null;
			_sceneBindings	= null;

			_settings.push(config);

			return config;
		}

		private function shaderInstanceRetainedHandler(instance : ShaderInstance,
													   numUses	: uint) : void
		{
			if (numUses == 1)
			{
				++_numActiveInstances;

				instance.begin.add(shaderInstanceBeginHandler);
				instance.end.add(shaderInstanceEndHandler);
			}
		}

		private function shaderInstanceReleasedHandler(instance : ShaderInstance,
													   numUses	: uint) : void
		{
			if (numUses == 0)
			{
				--_numActiveInstances;

				instance.begin.remove(shaderInstanceBeginHandler);
				instance.end.remove(shaderInstanceEndHandler);
			}
		}

		private function shaderInstanceBeginHandler(instance 	: ShaderInstance,
													context		: Context3DResource,
													backBuffer	: RenderTarget) : void
		{
			if (_numRenderedInstances == 0)
				_begin.execute(this, context, backBuffer);
		}

		private function shaderInstanceEndHandler(instance		: ShaderInstance,
												  context		: Context3DResource,
												  backBuffer	: RenderTarget) : void
		{
			_numRenderedInstances++;

			if (_numRenderedInstances == _numActiveInstances)
			{
				_numRenderedInstances = 0;
				_end.execute(this, context, backBuffer);
			}
		}
	}
}

fix missing ; at eol

fix missing ; at eol