/* <copyright>
This file contains proprietary software owned by Motorola Mobility, Inc.<br/>
No rights, expressed or implied, whatsoever to this software are provided by Motorola Mobility, Inc. hereunder.<br/>
(c) Copyright 2011 Motorola Mobility, Inc.  All Rights Reserved.
</copyright> */

///////////////////////////////////////////////////////////////////////
// Class DrawUtils
//      Overlay drawing utility functions
///////////////////////////////////////////////////////////////////////
var Montage = require("montage/core/core").Montage,
    Component = require("montage/ui/component").Component;

var vecUtils = require("js/helper-classes/3D/vec-utils").VecUtils;
var Rectangle = require("js/helper-classes/3D/rectangle").Rectangle;
var StageLine = require("js/helper-classes/3D/StageLine").StageLine;


var DrawUtils = exports.DrawUtils = Montage.create(Component, {

	///////////////////////////////////////////////////////////////////////
	// Instance variables
	///////////////////////////////////////////////////////////////////////
    viewUtils: { value: null, writable: true },
    snapManager: { value: null },
    ElementPlanes : { value: null, writable: true },

	// the drawing surface (a canvas)
	_drawingSurfaceElt : { value: null, writable: true },
	_drawingContext : { value: null, writable: true },

	// color to draw the lines
	_lineColor : { value: "black", writable: true},

	// define a stack for quickly setting graphics states and restoring them
	_stateArray : { value: [], writable: true },

	// save references to the grid lines for quick redraw
	_gridLineArray : {value: [], writable: true },

	// state for moveTo, lineTo
	_curPt : { value: null, writable: true },
	_curVis : { value: null, writable: true },

	// the element that defines the coordinate system for the displayed lines
	_sourceSpaceElt  : { value: null, writable: true },

	// maintain a list of objects to hide against
	_eltArray : {value: [], writable: true },

	// maintain a list of the planes to test against
	_planesArray : {value: [], writable: true },

	// the working plane.
	// a grid may be drawn aligned with this working plane
	_workingPlane : { value: null, writable: true },

	// save some parameters about the grid.
	// these parameters are set when the grid is drawn
	_gridHorizontalSpacing : {value: 50, writable: true },
	_gridVerticalSpacing : {value: 50, writable: true },
	_gridHorizontalLineCount : {value:10, writable: true },
	_gridVerticalLineCount : {value:0, writable: true },
	_gridOrigin : {value: null, writable: true },

	drawXY : {value: false, writable: true },
	drawXZ : {value: false, writable: true },
	drawYZ : {value: false, writable: true },

	drawElementN : {value: false, writable: true },

	_selectionCtr : {value: null, writable: true },

    // Properties that require element planes to be updated
	_updatePlaneProps : {value: ["matrix", "left", "top", "width", "height"], writable: false },

	///////////////////////////////////////////////////////////////////////
	// Property accessors
	///////////////////////////////////////////////////////////////////////
	setDrawingSurfaceElement : { value: function( s ) {  this._drawingSurfaceElt = s;  if (s)  this._drawingContext = s.getContext("2d");     }},
	getDrawingSurfaceElement : { value: function()          {  return this._drawingSurfaceElt;      }},

	getDrawingContext : { value: function()			{  return this._drawingContext;			}},

	setSourceSpaceElement : { value: function(ss)          {  this._sourceSpaceElt = ss;           }},
	getSourceSpaceElement : { value: function()            {  return this._sourceSpaceElt;         }},

	getWorkingPlane : { value: function()            {  return this._workingPlane;           }},
	setWorkingPlane : { value: function (wp)			{ this._workingPlane = wp;				}},

	getGridHorizontalSpacing : { value: function()	{  return this._gridHorizontalSpacing;		}},
	getGridVerticalSpacing : { value: function()	{  return this._gridVerticalSpacing;		}},
	getGridHorizontalLineCount : { value: function()	{  return this._gridHorizontalLineCount;	}},
	getGridVerticalLineCount : { value: function()	{  return this._gridVerticalLineCount;	}},
	getGridOrigin : { value: function()	{  return this._gridOrigin.slice(0);			}},

	isDrawingGrid : { value: function()			{  return this.drawXY || this.drawYZ || this.drawXZ;	}},
	isDrawingElementNormal : { value: function()			{  return this.drawElementN	}},

	getLineColor : { value: function()			{  return this._lineColor;					}},
	setLineColor : { value: function( color )		{  this._lineColor = color;					}},

	getLineWidth : { value: function()			{  return this._drawingContext.lineWidth;	}},
	setLineWidth : { value: function( w )			{  this._drawingContext.lineWidth = w;		}},


	initialize: {
		value: function() {
			this._gridOrigin = [0,0];	// 2D plane space point

            this.eventManager.addEventListener("elementAdded", this, false);
            this.eventManager.addEventListener("elementsRemoved", this, false);
            this.eventManager.addEventListener("elementChange", this, false);
            this.eventManager.addEventListener("elementChanging", this, false);
            this.eventManager.addEventListener("elementReplaced", this, false);
		}
	},

    initializeFromDocument:{
        value:function(adjustScrollOffsets){
            var i,
                documentRootChildren = this.application.ninja.currentDocument.model.views.design.getLiveNodeList(true),
                stage = this.application.ninja.stage,
                len = documentRootChildren.length;
            //initialize with current document
            this._eltArray = [];
            this._planesArray = [];
            this.setDrawingSurfaceElement(stage.gridCanvas);
            this.setSourceSpaceElement( this.application.ninja.currentDocument.model.documentRoot);
            this.setWorkingPlane( [0,0,1,0] );

            //Loop through all the top-level children of the current document and call drawUtils.addElement on them
            if(len > 0) {
                var initL = 0,
                    initT = 0,
                    minLeft = 0,
                    minTop = 0,
                    docLeft = stage.documentOffsetLeft,
                    docTop = stage.documentOffsetTop,
                    l,
                    t,
                    plane,
                    elt;
                for(i=0; i<len; i++) {
                    elt = documentRootChildren[i];
                    plane = this.addElement(elt);
                    if(adjustScrollOffsets) {
                        l = plane._rect.m_left - docLeft;
                        t = plane._rect.m_top - docTop;
                        if(l < minLeft) {
                            minLeft = l;
                        }
                        if(t < minTop) {
                            minTop = t;
                        }
                    }
                }
                if(minLeft !== initL) {
                    stage.userPaddingLeft = minLeft;
                }
                if(minTop !== initT) {
                    stage.userPaddingTop = minTop;
                }
            }
        }
    },

    handleElementAdded: {
        value: function(event) {
            var elements = event.detail;

            if(Array.isArray(elements)) {
                elements.forEach(function(element) {
                    this.addElement(element);
                }, this);
            } else {
                this.addElement(elements);
            }

            this.drawWorkingPlane();
        }
    },

    handleElementsRemoved: {
        value: function(event) {
            var elements = event.detail;

            if(Array.isArray(elements)) {
                elements = Array.prototype.slice.call(elements, 0);
                elements.forEach(function(element) {
                    this.removeElement(element);
                }, this);
            } else {
                this.removeElement(elements);
            }

            this.drawWorkingPlane();
        }
    },

    handleElementReplaced: {
        value: function(event) {
            var oldElement = event.detail.data.oldChild;
            var newElement = event.detail.data.newChild;

            // check if we already know about this object
            var n = this._eltArray.length,
                plane;
            for (var i=0;  i<n;  i++) {
                if (oldElement === this._eltArray[i]) {
                    this._eltArray[i] = newElement;
                    plane = this._planesArray[i];
                    break;
                }
            }

            if(!plane) {
                this._eltArray.push( newElement );
                plane = Object.create(this.ElementPlanes, {});
                this._planesArray.push( plane );
            }

            plane.setElement( newElement );
            plane.init();
            newElement.elementModel.props3D.elementPlane = plane;
        }
    },

    _shouldUpdatePlanes: {
        value: function(props) {
            if(!props) {
                return false;
            } else if (typeof props === "string") {
                return (this._updatePlaneProps.indexOf(props) !== -1);
            }

            for (var p in props) {
                if(this._updatePlaneProps.indexOf(p) !== -1) {
                    return true;
                }
            }

            return false;
        }
    },

    // TODO - Check why handleElementChange is being fired before handleAddElement
    handleElementChange: {
        value: function(event) {
            this._elementChangeHelper(event, false);
        }
    },

    handleElementChanging: {
        value: function(event) {
            this._elementChangeHelper(event, true);
        }
    },

    _elementChangeHelper: {
        value: function(event, isChanging) {
            if(!event.detail || !event.detail.data) {
                return;
            }
            var els = event.detail.data.els;
            if(els && this._shouldUpdatePlanes(event.detail.data.prop)) {
                var len = els.length,
                    stage = this.application.ninja.stage,
                    minLeft = stage.userPaddingLeft,
                    minTop = stage.userPaddingTop,
                    docLeft = stage.userContentLeft,
                    docTop = stage.userContentTop,
                    l,
                    t,
                    plane,
                    changed = false,
                    adjustStagePadding = !isChanging || (event.detail.data.prop !== "matrix");
                for(var i=0; i < len; i++) {
                    plane = els[i].elementModel.props3D.elementPlane;
                    if(plane) {
                        plane.init();
                        if(adjustStagePadding) {
                            l = plane._rect.m_left - docLeft;
                            t = plane._rect.m_top - docTop;
                            if(l < minLeft) {
                                minLeft = l;
                            }
                            if(t < minTop) {
                                minTop = t;
                            }
                        }
                    }
                }

                if(adjustStagePadding) {
                    if(minLeft !== stage.userPaddingLeft) {
                        stage.userPaddingLeft = minLeft;
                        changed = true;
                    }
                    if(minTop !== stage.userPaddingTop) {
                        stage.userPaddingTop = minTop;
                        changed = true;
                    }
                }

                if(!changed) {
                    this.drawWorkingPlane();
                    this.draw3DCompass();
                }

                // TODO - Remove this once all stage drawing is consolidated into a single draw cycle
                if(!isChanging && this.application.ninja.toolsData.selectedToolInstance.captureSelectionDrawn) {
                    this.application.ninja.toolsData.selectedToolInstance.captureSelectionDrawn(null);
                }
            }
        }
    },

	///////////////////////////////////////////////////////////////////////
	// Methods
	///////////////////////////////////////////////////////////////////////

	addElement: {
		value: function( elt ) {
			// check if we already know about this object
			var n = this._eltArray.length;
			for (var i=0;  i<n;  i++) {
				if (elt == this._eltArray[i]) {
					return;
				}
			}

			this._eltArray.push( elt );

			// create the planes for this element
			var plane = Object.create(this.ElementPlanes, {});
			plane.setElement( elt );
			plane.init();
			this._planesArray.push( plane );
            elt.elementModel.props3D.elementPlane = plane;
            return plane;
		}
	},

    removeElement: {
        value: function(element) {
			// check if object exists
			var _elements = this._eltArray.length;
			for (var i=0;  i < _elements;  i++) {
				if (element === this._eltArray[i]) {
					// First remove the planes for this element
					this._planesArray.splice(i, 1);
					// Then remove the element
					this._eltArray.splice(i, 1);

                    // TODO - May need to delete props3D and elementPlane as well
					return;
				}
			}
		}
	},

	clear : {
		value: function() {
			if (this._drawingContext)
				this._drawingContext.clearRect( 0, 0, this._drawingSurfaceElt.width,  this._drawingSurfaceElt.height );
		}
	},

	updatePlanes : {
		value: function() {
			var n = this._planesArray.length;

			for (var i=0;  i<n;  i++)
			{
				var plane = this._planesArray[i];
				plane.init();
			}
		}
	},

	getVisibilityAtPoint:
	{
		value: function( targetPt )
		{
			// duplicate the point and make sure it has the correct dimension (2)
			var pt = targetPt.slice(0);
			while (pt.length > 3)  pt.pop();

			var z = pt[2];
			var n = this._planesArray.length;
			var vis = 0;
			for (var i=0;  i<n;  i++)
			{
				var plane = this._planesArray[i];

				// ignore if the point is in front of the polygon
				if (z > plane.getZMax())  continue;

				// test for containment in the polygon bounds
				var contain = MathUtils.boundaryContainsPoint( plane.getBoundaryPoints(),  pt,  plane.isBackFacing() );
				if (contain == MathUtils.OUTSIDE)  continue;
				if (contain == MathUtils.ON)       continue;

				// shoot a ray from the point in the +Z direction to get the z value of the plane
				var vec = [0,0,1];
				var planeEq = plane.getPlaneEq();
				var ptOnPlane = MathUtils.vecIntersectPlane( pt, vec, planeEq );
				if (ptOnPlane)
				{
					// in keeping with the convention that a point "on" a face is outside the element,
					// check that case.
					//if (MathUtils.fpCmp(pt[2], ptOnPlane[2]) == 0)  continue;

					// if the point is behind the plane, increase the visibility
					if (MathUtils.fpCmp(pt[2],ptOnPlane[2]) <= 0)  vis++;
				}
			}

			return vis;
		}
	},

	moveTo : {
		value: function( pt ) {
			if (this._sourceSpaceElt)
				pt = this.viewUtils.localToGlobal( pt, this._sourceSpaceElt );

			this._curPt = pt.slice(0);
			this._curVis = this.getVisibilityAtPoint( pt );
		}
	},

	lineTo :
	{
		value: function( pt )
		{
			if (this._sourceSpaceElt)
				pt = this.viewUtils.localToGlobal( pt, this._sourceSpaceElt );

			var line = Object.create(StageLine, {});
			line.setPoints( this._curPt, pt );
			line.setVisibility( this._curVis );

			// find all the plane intersections
			this.getLineIntersections( line );

			// draw the line
			this._curVis = this.drawIntersectedLine( line, this._drawingContext );
			this._curPt = pt.slice(0);
		}
	},


	drawLine :
	{
		value: function( pt0, pt1 )
		{
			if (this._drawingContext)
			{
				// transform the points from local object space to world space
				if (this._sourceSpaceElt)
				{
					pt0 = this.viewUtils.localToGlobal( pt0, this._sourceSpaceElt );
					pt1 = this.viewUtils.localToGlobal( pt1, this._sourceSpaceElt );
				}

				// create the line structure
				var line = Object.create(StageLine, {});
				line.setPoints( pt0, pt1 );

				// find all the plane intersections
				this.getLineIntersections( line );

				// get the starting visibility
				var vis = this.getVisibilityAtPoint( pt0 );
				line.setVisibility( vis );

				// draw the line
				this._curVis = this.drawIntersectedLine( line, this._drawingContext );
			}
		}
	},

	drawIntersectedLine :
	{
		value: function( line )
		{
			this._drawingContext.strokeStyle = this._lineColor;
			this._drawingContext.beginPath();

			//  get the 2 enpoints of the line
			var pt0 = line.getPoint0(),
				pt1 = line.getPoint1();

			// find the visibility at the start point
			var vis = line.getVisibility();
			if (vis == 0)
			{
				this._drawingContext.strokeStyle = this._lineColor;
				this._drawingContext.beginPath();
				this._drawingContext.moveTo( pt0[0], pt0[1] );
			}

			// go through each intersection
			var n = line.getIntersectionCount();
			var t = 0.0;
			var iRec = line.getIntersectionList();
			for (var i=0;  i<n;  i++)
			{
				var tNext = iRec.getT();
				var dv = iRec.getDeltaVis();

				var tPt = MathUtils.interpolateLine3D( pt0, pt1, tNext );

				if (vis == 0)
				{
					this._drawingContext.lineTo( tPt[0], tPt[1] );
					this._drawingContext.closePath();
					this._drawingContext.stroke();
				}
				else
				{
					this._drawingContext.beginPath();
					this._drawingContext.moveTo( tPt[0], tPt[1] );
				}

				vis += dv;
				iRec = iRec.getNext();
			}

			// handle the end of the line
			if (vis == 0)
			{
				this._drawingContext.lineTo( pt1[0], pt1[1] );

				// draw the line
				this._drawingContext.closePath();
				this._drawingContext.stroke();
			}

			return vis;
		}
	},

	getLineIntersections:
	{
		value: function( line )
		{
			// clip the line against all polygons in the scene
			var n = this._planesArray.length;
			for (var i=0;  i<n;  i++)
			{
				var plane = this._planesArray[i];
				line.intersectWithPlane( plane );
			}
		}
	},

	getPlaneToWorldMatrix:
	{
		value: function (normal, ptOnPlane)
		{
			// 3 coordinate axes for the plane
			var zAxis = normal.slice(0);
			MathUtils.makeDimension3(zAxis);
			zAxis = vecUtils.vecNormalize(3, zAxis);

			// special case the coordinate axes followed by general case
			var tmp;
			var xAxis, yAxis;
			if (MathUtils.fpCmp(Math.abs(zAxis[0]), 1.0) == 0) {
				yAxis = [0, 1, 0];
				xAxis = vecUtils.vecCross(3, yAxis, zAxis);
			}
			else if (MathUtils.fpCmp(Math.abs(zAxis[1]), 1.0) == 0) {
				yAxis = [0, 0, 1];
				//xAxis = yAxis.cross(zAxis);
				xAxis = vecUtils.vecCross(3, yAxis, zAxis );
			}
			else if (MathUtils.fpCmp(Math.abs(zAxis[2]), 1.0) == 0) {
				yAxis = [0, 1, 0];
				//xAxis = yAxis.cross(zAxis);
				xAxis = vecUtils.vecCross(3, yAxis, zAxis );
			}
			else
			{
				if (Math.abs(zAxis[0]) < Math.abs(zAxis[1]))
				{
					if (Math.abs(zAxis[0]) < Math.abs(zAxis[2]))
						tmp = [1, 0, 0];
					else
						tmp = [0, 0, 1];
				}
				else
				{
					if (Math.abs(zAxis[1]) < Math.abs(zAxis[2]))
						tmp = [0, 1, 0];
					else
						tmp = [0, 0, 1];
				}
				//xAxis = tmp.cross(zAxis);
				xAxis = vecUtils.vecCross(3, tmp, zAxis);
				//yAxis = zAxis.cross(xAxis);
				yAxis = vecUtils.vecCross(3, zAxis, xAxis);
			}

			// create the matrix
			var mat = Matrix.create(
				[
					[xAxis[0], yAxis[0], zAxis[0], ptOnPlane[0]],
					[xAxis[1], yAxis[1], zAxis[1], ptOnPlane[1]],
					[xAxis[2], yAxis[2], zAxis[2], ptOnPlane[2]],
					[0, 0, 0, 1]
				]
			);

			return mat;
		}
	},

	clearDefaultGridOffset : {
		value: function() {
			this.setDefaultGridOffset( [0,0,0] );
		}
	},

	drawWorkingPlane:
	{
		value: function ()
		{
            this.application.ninja.stage.clearGridCanvas();
            this.drawStageOutline();
			if (!this.isDrawingGrid()) return;

			var saveContext = this.getDrawingSurfaceElement();
			this.setDrawingSurfaceElement(this.application.ninja.stage.gridCanvas);

			// 3 coordinate axes for the plane
			var zAxis = [this._workingPlane[0], this._workingPlane[1], this._workingPlane[2]];

			// get a point that lies on the plane
			var ptOnPlane = MathUtils.getPointOnPlane(this._workingPlane);

            // define the grid parameters
            var width = this.snapManager.getStageWidth(),
                height = this.snapManager.getStageHeight(),
                nLines = 10;

			// get a matrix from working plane space to the world
			var mat = this.getPlaneToWorldMatrix(zAxis, ptOnPlane);
			var tMat = Matrix.Translation( [0.5*width, 0.5*height, 0] );
			//mat = tMat.multiply(mat);
			glmat4.multiply( tMat, mat, mat);

			// the positioning of the grid may depend on the view direction.
			var stage = this.snapManager.getStage();
			var viewMat = this.viewUtils.getMatrixFromElement(stage);
			var viewDir = [viewMat[8], viewMat[9], viewMat[10]];

			var dx, dy, delta, pt0, pt1;
			dx = this._gridVerticalSpacing;
			dy = this._gridHorizontalSpacing;
			nLines = Math.floor(width / dx) + 1;
			if (MathUtils.fpCmp(dx*nLines,width) == 0)  nLines--;

			var saveColor = this._lineColor;
			var saveLineWidth = this._drawingContext.lineWidth;

			// reset the line cache
			this._gridLineArray = new Array();

			if (this.drawXY) this._lineColor = "red";
			if (this.drawYZ) this._lineColor = "green";
			if (this.drawXZ) this._lineColor = "blue";
			this._drawingContext.lineWidth = 0.25;

			// get the two endpoints of the first line with constant X
			pt0 = [-width / 2.0, height / 2.0, 0];
			pt1 = [-width / 2.0, -height / 2.0, 0];
			delta = [dx, 0, 0];

			this._gridVerticalLineCount = nLines;
			this._gridOrigin = pt1.slice(0);

			// draw the lines with constant X
			this.drawGridLines(pt0, pt1, delta, mat, nLines);

			// get the two endpoints of the first line with constant Y
			pt0 = [-width / 2.0, -height / 2.0, 0];
			pt1 = [width / 2.0, -height / 2.0, 0];

			delta = [0, dy, 0];
			nLines = Math.floor(height / dy) + 1;
			if (MathUtils.fpCmp(dy*nLines,height) == 0)  nLines--;

			this._gridHorizontalLineCount = nLines;

			// draw the lines with constant Y
			this.drawGridLines(pt0, pt1, delta, mat, nLines);

			this._lineColor = saveColor;
			this._drawingContext.lineWidth = saveLineWidth;

			// draw the lines
			this.redrawGridLines();

			this.setDrawingSurfaceElement(saveContext);
		}
	},

	drawGridLines : {
		value: function (pt0, pt1, delta, mat, nLines) {
			// get the drawing context
			if (this._drawingContext) {
				var p0 = pt0.slice(0), p1 = pt1.slice(0); // duplicate so we don't change the input.
				p0[3] = 1; p1[3] = 1;
				var d = delta.slice(0); d[3] = 1;

				//var lineArray = new Array;
				var offset = this.viewUtils.getElementOffset(this._sourceSpaceElt);
				offset[2] = 0;
				this.viewUtils.setViewportObj(this._sourceSpaceElt);
                var sourceSpaceMat = this.viewUtils.getLocalToGlobalMatrix( this._sourceSpaceElt );
				for (var i = 0; i < nLines; i++) {
					// transform the points from working plane space to world space
					var t0 = glmat4.multiplyVec3( mat, p0, [] ),
						t1 = glmat4.multiplyVec3( mat, p1, [] );

					// transform from world space to global screen space
					if (this._sourceSpaceElt) {
						t0 = this.viewUtils.localToGlobal2(t0, sourceSpaceMat);
						t1 = this.viewUtils.localToGlobal2(t1, sourceSpaceMat);
					}

					// create a line from the endpoints
					var line = Object.create(StageLine, {});
					line.setPoints(t0, t1);
					this._gridLineArray.push(line);

					// find all the intersections
					this.getLineIntersections(line);

					// get the visibility at the start point
					var vis = this.getVisibilityAtPoint(line.getPoint0());
					line.setVisibility(vis);

					// increment the points to the next position
					p0 = vecUtils.vecAdd(4, p0, d); p0[3] = 1.0;
					p1 = vecUtils.vecAdd(4, p1, d); p1[3] = 1.0;
				}
			}
		}
	},

	refreshDisplay : {
		value: function() {
			this.redrawGridLines();
			this.snapManager.drawLastHit();
		}
	},

	pushState : {
		value: function() {
			var obj = new Object();
			obj._lineColor = this._lineColor;
			obj._lineWidth = this._drawingContext.lineWidth;

			this._stateArray.push( obj );
		}
	},

	popState  : {
		value: function() {
			if (this._stateArray.length <= 0)
			{
				throw new Error( "state stack underflow" );
				return;
			}

			var obj = this._stateArray.pop();
			this._lineColor = obj._lineColor;
			this._drawingContext.lineWidth = obj._lineWidth;
		}
	},


	redrawGridLines : {
		value: function() {
			if (!this.isDrawingGrid()) return;

			this.pushState();

			if (this.drawXY) this._lineColor = "red";
			if (this.drawYZ) this._lineColor = "green";
			if (this.drawXZ) this._lineColor = "blue";
			this._drawingContext.lineWidth = 0.25;

			// draw the lines
            var line,
			    nLines = this._gridLineArray.length;

			for (var i = 0; i < nLines; i++) {
				line = this._gridLineArray[i];
				this.drawIntersectedLine(line, this._drawingContext);
			}

			this.popState();
		}
	},

	drawSelectionBounds : {
		value: function( eltArray ) {
			this._selectionCtr = null;
            var len = eltArray.length,
                i,
                j,
                bounds,
                bounds3D,
                pt,
                tmpPt,
                ssMat,
                elt;
			
			if (len === 0)  return;
			var context = this._drawingContext;
			if (!context)  return;

            // TODO - Get values from app settings
			context.strokeStyle = "#46a1ff";
			context.lineWidth = 2;

			// handle the single element case
            // TODO - Currently, the stage draws its own selection bounds for single selection case
			if (len === 1)
			{
				// single selection case
				//console.log( "single selection" );

				elt = eltArray[0];

				this.viewUtils.pushViewportObj( elt );

				// get the element bounds in world space
				bounds3D = this.viewUtils.getElementViewBounds3D( elt );
				for (j=0;  j<4;  j++) {
					bounds3D[j] = this.viewUtils.localToGlobal( bounds3D[j],  elt );
				}

				// draw it
				context.beginPath();
				//VV
				context.strokeStyle = "#46a1ff";
				context.lineWidth = 2.0;

				context.moveTo( bounds3D[3][0] ,  bounds3D[3][1] );
				for (var v=0;  v<4;  v++) {
					context.lineTo( bounds3D[v][0] ,  bounds3D[v][1] );
				}
				context.closePath();
				context.stroke();

				this._selectionCtr = MathUtils.getCenterFromBounds(3, bounds3D);
//				console.log("selection center, single elt case - ", this._selectionCtr);
				
				this.viewUtils.popViewportObj();
			}
			else
			{
				// get the plane from the first element to compare against the other elements
				var dot;
				var flat = true;
				var plane = this.viewUtils.getUnprojectedElementPlane( eltArray[0] );
				for (i=1;  i<len;  i++)
				{
					elt = eltArray[i];
					var plane2 = this.viewUtils.getUnprojectedElementPlane( elt );
					dot = Math.abs( MathUtils.dot3(plane,plane2) );
					if (MathUtils.fpCmp(dot, 1) != 0)
					{
						flat = false;
						break;
					}

					// check the offset
					var d = plane[3],  d2 = plane2[3];
					if (MathUtils.fpCmp(d,d2) != 0)
					{
						flat = false;
						break;
					}
				}

				// if all of the planes are aligned, check if they are aligned with the view direction
				if (flat)
				{
					var stage = this.application.ninja.currentDocument.model.documentRoot;
					var stageMat = this.viewUtils.getMatrixFromElement(stage);
					var viewDir = [ stageMat[8], stageMat[9], stageMat[10] ];
					viewDir = vecUtils.vecNormalize( 3, viewDir );
					dot = Math.abs( MathUtils.dot3(plane,viewDir) );
					if (MathUtils.fpCmp(dot, 1) != 0)
						flat = false;
				}
//				console.log( "drawSelectionBounds, flat: " + flat );

				// if all the elements share the same plane, draw the 2D rectangle
				if (flat)
				{
					// make a 2D rectangle on the plane
					var rect;
					for (i=0;  i<len;  i++)
					{
						elt = eltArray[i];

						// get the element bounds in 'plane' space
						bounds = this.viewUtils.getElementViewBounds3D( elt );
						ssMat = this.viewUtils.getLocalToGlobalMatrix( elt );
						for (j=0;  j<4;  j++)
						{
							var localPt = bounds[j];
							tmpPt = this.viewUtils.localToGlobal2(localPt, ssMat);
							pt = tmpPt;

							if (!rect)
							{
								rect = Object.create(Rectangle, {});
								rect.setToPoint( pt )
							}
							else
							{
								rect.unionPoint( pt );
							}
						}
					}

					// draw the rectangle
					context.beginPath();

					pt = MathUtils.makeDimension3(rect.getPoint(3));

					bounds3D = [[0,0], [0,0], [0,0], [0,0]];
					this._selectionCtr = pt.slice(0);

					context.moveTo( pt[0],  pt[1] );
					for (i=0;  i<4;  i++)
					{
						pt = rect.getPoint(i);
						context.lineTo( pt[0],  pt[1] );
						bounds3D[i] = pt.slice(0);
					}
					context.closePath();
					context.stroke();

					var dir = vecUtils.vecSubtract(2, bounds3D[1], bounds3D[3]);
					var ctr = vecUtils.vecNormalize(2, dir, vecUtils.vecDist(2, bounds3D[1], bounds3D[3])/2);

					this._selectionCtr[0] += ctr[0] - this.application.ninja.stage.userContentLeft;
					this._selectionCtr[1] += ctr[1] - this.application.ninja.stage.userContentTop;
				}
				else
				{
					var minPt,  maxPt;

					// we set the root to "the world".
					var saveRoot = this.viewUtils.getRootElement();
					this.viewUtils.setRootElement( this.viewUtils.getStageElement() );
					ssMat = this.viewUtils.getLocalToGlobalMatrix( this._sourceSpaceElt );

					for (i=0;  i<len;  i++)
					{
						elt = eltArray[i];
						bounds = this.viewUtils.getElementViewBounds3D( elt );
						var eltMat = this.viewUtils.getLocalToGlobalMatrix( elt );
						for (j=0;  j<4;  j++)
						{
							pt = this.viewUtils.localToGlobal2( bounds[j],  eltMat );
							var tmpPt = this.viewUtils.localToStageWorld(bounds[j], elt);
							tmpPt = this.viewUtils.screenToView( tmpPt[0], tmpPt[1], tmpPt[2] );
							if (!minPt)
							{
								minPt = pt.slice(0);
								maxPt = pt.slice(0);
							}
							else
							{
								var x = pt[0],  y = pt[1], z = pt[2];

								if (x < minPt[0])    minPt[0] = x;
								if (x > maxPt[0])    maxPt[0] = x;

								if (y < minPt[1])    minPt[1] = y;
								if (y > maxPt[1])    maxPt[1] = y;

								if (z < minPt[2])    minPt[2] = z;
								if (z > maxPt[2])    maxPt[2] = z;
							}
						}
					}

					// restore the root ID
					this.viewUtils.setRootElement (saveRoot );

					context.beginPath();

					var x0 = minPt[0],  y0 = minPt[1],  z0 = minPt[2],
						x1 = maxPt[0],  y1 = maxPt[1],  z1 = maxPt[2];

					this._selectionCtr = [x0, y0, z0];
					this._selectionCtr[0] += (x1-x0)/2;
					this._selectionCtr[1] += (y1-y0)/2;
					this._selectionCtr[2] += (z1-z0)/2;

					// get the 8 corners of the parallelpiped in world space
					var wc = new Array();   // wc == world cube
					wc.push(  this.viewUtils.localToGlobal2( [x0,y0,z1], ssMat ) );
					wc.push(  this.viewUtils.localToGlobal2( [x0,y1,z1], ssMat ) );
					wc.push(  this.viewUtils.localToGlobal2( [x1,y1,z1], ssMat ) );
					wc.push(  this.viewUtils.localToGlobal2( [x1,y0,z1], ssMat ) );
					wc.push(  this.viewUtils.localToGlobal2( [x0,y0,z0], ssMat ) );
					wc.push(  this.viewUtils.localToGlobal2( [x0,y1,z0], ssMat ) );
					wc.push(  this.viewUtils.localToGlobal2( [x1,y1,z0], ssMat ) );
					wc.push(  this.viewUtils.localToGlobal2( [x1,y0,z0], ssMat ) );

					// determine the signs of the normals of the faces relative to the view direction.
					var front	= -MathUtils.fpSign( vecUtils.vecCross(3, vecUtils.vecSubtract(3,wc[2],wc[1]), vecUtils.vecSubtract(3,wc[0],wc[1]))[2] ),
						right   = -MathUtils.fpSign( vecUtils.vecCross(3, vecUtils.vecSubtract(3,wc[6],wc[2]), vecUtils.vecSubtract(3,wc[3],wc[2]))[2] ),
						back    = -MathUtils.fpSign( vecUtils.vecCross(3, vecUtils.vecSubtract(3,wc[5],wc[6]), vecUtils.vecSubtract(3,wc[7],wc[6]))[2] ),
						left    = -MathUtils.fpSign( vecUtils.vecCross(3, vecUtils.vecSubtract(3,wc[1],wc[5]), vecUtils.vecSubtract(3,wc[4],wc[5]))[2] ),
						top     = -MathUtils.fpSign( vecUtils.vecCross(3, vecUtils.vecSubtract(3,wc[3],wc[0]), vecUtils.vecSubtract(3,wc[4],wc[0]))[2] ),
						bottom  = -MathUtils.fpSign( vecUtils.vecCross(3, vecUtils.vecSubtract(3,wc[5],wc[1]), vecUtils.vecSubtract(3,wc[2],wc[1]))[2] );

					// draw the side faces
					var p;

					//context.strokeStyle = ((front > 0) || (right > 0)) ? dark : light;  context.beginPath();
					if ((front > 0) || (right > 0)) {
						context.beginPath();
						p = this.viewUtils.localToGlobal2( [x1, y0, z1], ssMat );  context.moveTo( p[0], p[1] );
						p = this.viewUtils.localToGlobal2( [x1, y1, z1], ssMat );  context.lineTo( p[0], p[1] );
						context.closePath();  context.stroke();
					}

					//context.strokeStyle = ((right > 0) || (back > 0)) ? dark : light;  context.beginPath();
					if ((right > 0) || (back > 0)) {
						context.beginPath();
						p = this.viewUtils.localToGlobal2( [x1, y0, z0], ssMat );  context.moveTo( p[0], p[1] );
						p = this.viewUtils.localToGlobal2( [x1, y1, z0], ssMat );  context.lineTo( p[0], p[1] );
						context.closePath();  context.stroke();
					}

					//context.strokeStyle = ((back > 0) || (left > 0)) ? dark : light;  context.beginPath();
					if ((back > 0) || (left > 0)) {
						context.beginPath();
						p = this.viewUtils.localToGlobal2( [x0, y0, z0], ssMat );  context.moveTo( p[0], p[1] );
						p = this.viewUtils.localToGlobal2( [x0, y1, z0], ssMat );  context.lineTo( p[0], p[1] );
						context.closePath();  context.stroke();
					}

					//context.strokeStyle = ((left > 0) || (front > 0)) ? dark : light;  context.beginPath();
					if ((left > 0) || (front > 0)) {
						context.beginPath();
						p = this.viewUtils.localToGlobal2( [x0, y0, z1], ssMat );  context.moveTo( p[0], p[1] );
						p = this.viewUtils.localToGlobal2( [x0, y1, z1], ssMat );  context.lineTo( p[0], p[1] );
						context.closePath();  context.stroke();
					}

					// draw the top and bottom faces
					//context.strokeStyle = ((front > 0) || (top > 0)) ? dark : light;  context.beginPath();
					if ((front > 0) || (top > 0)) {
						context.beginPath();
						p = this.viewUtils.localToGlobal2( [x0, y0, z1], ssMat );  context.moveTo( p[0], p[1] );
						p = this.viewUtils.localToGlobal2( [x1, y0, z1], ssMat );  context.lineTo( p[0], p[1] );
						context.closePath();  context.stroke();
					}

					//context.strokeStyle = ((top > 0) || (back > 0)) ? dark : light;  context.beginPath();
					if ((top > 0) || (back > 0)) {
						context.beginPath();
						p = this.viewUtils.localToGlobal2( [x0, y0, z0], ssMat );  context.moveTo( p[0], p[1] );
						p = this.viewUtils.localToGlobal2( [x1, y0, z0], ssMat );  context.lineTo( p[0], p[1] );
						context.closePath();  context.stroke();
					}

					//context.strokeStyle = ((back > 0) || (bottom > 0)) ? dark : light;  context.beginPath();
					if ((back > 0) || (bottom > 0)) {
						context.beginPath();
						p = this.viewUtils.localToGlobal2( [x0, y1, z0], ssMat );  context.moveTo( p[0], p[1] );
						p = this.viewUtils.localToGlobal2( [x1, y1, z0], ssMat );  context.lineTo( p[0], p[1] );
						context.closePath();  context.stroke();
					}

					//context.strokeStyle = ((bottom > 0) || (front > 0)) ? dark : light;  context.beginPath();
					if ((bottom > 0) || (front > 0)) {
						context.beginPath();
						p = this.viewUtils.localToGlobal2( [x0, y1, z1], ssMat );  context.moveTo( p[0], p[1] );
						p = this.viewUtils.localToGlobal2( [x1, y1, z1], ssMat );  context.lineTo( p[0], p[1] );
						context.closePath();  context.stroke();
					}

					// and the remaining lines - varying Z
					if ((top > 0) || (right > 0)) {
						context.beginPath();
						p = this.viewUtils.localToGlobal2( [x1, y0, z0], ssMat );  context.moveTo( p[0], p[1] );
						p = this.viewUtils.localToGlobal2( [x1, y0, z1], ssMat );  context.lineTo( p[0], p[1] );
						context.closePath();  context.stroke();
					}

					//context.strokeStyle = ((right > 0) || (bottom > 0)) ? dark : light;  context.beginPath();
					if ((right > 0) || (bottom > 0)) {
						context.beginPath();
						p = this.viewUtils.localToGlobal2( [x1, y1, z0], ssMat );  context.moveTo( p[0], p[1] );
						p = this.viewUtils.localToGlobal2( [x1, y1, z1], ssMat );  context.lineTo( p[0], p[1] );
						context.closePath();  context.stroke();
					}

					//context.strokeStyle = ((bottom > 0) || (left > 0)) ? dark : light;  context.beginPath();
					if ((bottom > 0) || (left > 0)) {
						context.beginPath();
						p = this.viewUtils.localToGlobal2( [x0, y1, z0], ssMat );  context.moveTo( p[0], p[1] );
						p = this.viewUtils.localToGlobal2( [x0, y1, z1], ssMat );  context.lineTo( p[0], p[1] );
						context.closePath();  context.stroke();
					}

					//context.strokeStyle = ((left > 0) || (top > 0)) ? dark : light;  context.beginPath();
					if ((left > 0) || (top > 0)) {
						context.beginPath();
						p = this.viewUtils.localToGlobal2( [x0, y0, z0], ssMat );  context.moveTo( p[0], p[1] );
						p = this.viewUtils.localToGlobal2( [x0, y0, z1], ssMat );  context.lineTo( p[0], p[1] );
						context.closePath();  context.stroke();
					}
				}
			}
		}
	},


	drawElementNormal:
	{
		value: function( elt )
		{
			if (!this.isDrawingElementNormal()) return;

			// set the element to be the viewport object - temporarily
			this.viewUtils.pushViewportObj( elt );

			// save the source space object and set to the target object
			var saveSource = this._sourceSpaceElt;
			this._sourceSpaceElt = elt;

			// temporarily set the line color
			var saveColor = this._lineColor;
			this._lineColor = "blue";

			var base = this.viewUtils.getCenterOfProjection();
			base[2] = 1;   // Z - make it just off the plane to avoid numerical issues
			var zAxis = base.slice(0);
			zAxis[2] += 50;
			this.moveTo( base );
			this.lineTo( zAxis );

			// draw the arrowhead
			var headWidth = 6;
			var head = MathUtils.interpolateLine3D( base, zAxis, 0.7 );
			var p0 = head.slice(0);  p0[1] += headWidth;  this.drawLine( zAxis, p0 );
			var p1 = head.slice(0);  p1[0] += headWidth;  this.drawLine( zAxis, p1 );
			var p2 = head.slice(0);  p2[1] -= headWidth;  this.drawLine( zAxis, p2 );
			var p3 = head.slice(0);  p3[0] -= headWidth;  this.drawLine( zAxis, p3 );

			this.moveTo( p0 );
			this.lineTo( p1 );
			this.lineTo( p2 );
			this.lineTo( p3 );
			this.lineTo( p0 );

			// restore the state
			this.viewUtils.popViewportObj();
			this._lineColor = saveColor;
			this._sourceSpaceElt = saveSource;
		}
	},

    drawStageOutline : {
        value: function() {
            var context = this.application.ninja.stage.gridContext;
            var stage = this.application.ninja.stage;
            var stageRoot = this.application.ninja.currentDocument.model.documentRoot;

            // draw an outline around the template body if stage has any transforms
            if(stage.currentDocument.model.views.design._template && !MathUtils.isIdentityMatrix(this.viewUtils.getMatrixFromElement(stageRoot))) {
                var saveContext = this.getDrawingSurfaceElement();
                this.setDrawingSurfaceElement(this.application.ninja.stage.gridCanvas);

                var stagePt = MathUtils.getPointOnPlane([0,0,1,0]);
                var stageMat = this.getPlaneToWorldMatrix([0,0,1], stagePt);
                var width = this.snapManager.getStageWidth(),
                    height = this.snapManager.getStageHeight(),
                    pt0 = [0, 0, 0],
                    pt1 = [0, height, 0],
                    delta = [width, 0, 0];

                this._gridLineArray.length = 0;
                this.drawGridLines(pt0, pt1, delta, stageMat, 2);

                pt0 = [0, 0, 0];
                pt1 = [width, 0, 0];
                delta = [0, height, 0];
                this.drawGridLines(pt0, pt1, delta, stageMat, 2);

                this._lineColor = "red";
                for (var i = 0; i < 4; i++) {
                    this.drawIntersectedLine(this._gridLineArray[i], this._drawingContext);
                }

                this.setDrawingSurfaceElement(saveContext);
            }

            // draw reference lines across origin
            var bounds3D = this.viewUtils.getElementBoundsInGlobal(stageRoot);
            var l = MathUtils.segSegIntersection2D(bounds3D[0], bounds3D[3], [0, 0, 0], [0, stage.canvas.height, 0], 0.1);
            if(!l) return;
            var r = MathUtils.segSegIntersection2D(bounds3D[0], bounds3D[3], [stage.canvas.width, 0, 0], [stage.canvas.width, stage.canvas.height, 0], 0.1);
            if(!r) return;

            var t = MathUtils.segSegIntersection2D(bounds3D[0], bounds3D[1], [0, 0, 0], [stage.canvas.width, 0, 0], 0.1);
            if(!t) return;
            var b = MathUtils.segSegIntersection2D(bounds3D[0], bounds3D[1], [0, stage.canvas.height, 0], [stage.canvas.width, stage.canvas.height, 0], 0.1);
            if(!b) return;

            context.save();
            context.strokeStyle = "#333";
            context.lineWidth = 0.5;

            context.beginPath();

            context.moveTo(l[0], l[1]);
            context.lineTo(r[0], r[1]);

            context.moveTo(t[0], t[1]);
            context.lineTo(b[0], b[1]);

            context.closePath();
            context.stroke();

            context.fillText("(0, 0)", bounds3D[0][0] + 4, bounds3D[0][1] - 6);
            context.restore();
        }
    },

	draw3DCompass : {
		value: function() {
			// set the element to be the viewport object - temporarily
			var tmpCanvas = this.application.ninja.stage.canvas;
			var tmpStage = this.application.ninja.currentDocument.model.documentRoot;
//			this.viewUtils.pushViewportObj( tmpCanvas );

			// save the source space object and set to the target object
			var saveSource = this._sourceSpaceElt;
			this._sourceSpaceElt = tmpStage;

			// temporarily set the line color
			var saveColor = this._lineColor;
			var saveLineWidth = this._lineWidth;

			var origLeft = 60;
			var origTop = tmpCanvas.height - 60;

			var mat = this.viewUtils.getMatrixFromElement( this._sourceSpaceElt );
			var tMat = Matrix.Translation([origLeft,origTop,0]);

			mat[12] = 0;
			mat[13] = 0;
			mat[14] = 0;

			//var resMat = tMat.multiply(mat);
			var resMat = glmat4.multiply( tMat, mat, [] );
			var origin = [0,0,0,1];

			var zoomFactor = this.application.ninja.documentBar.zoomFactor/100.0;
			var arrowSize = 50 / zoomFactor;
			var xAxis = [arrowSize,0,0,1];
			//var rO = resMat.multiply(origin);
			var rO = glmat4.multiplyVec3( resMat, origin, []);  
			//var xO = resMat.multiply(xAxis);
			var xO = glmat4.multiplyVec3( resMat, xAxis, []);

			var yAxis = [0,arrowSize,0,1];
			var yO = glmat4.multiplyVec3( resMat, yAxis, []);

			var zAxis = [0,0,arrowSize,1];
			var zO = glmat4.multiplyVec3( resMat, zAxis, []);

			var saveContext = this.getDrawingSurfaceElement();
			//this.setDrawingSurfaceElement(window.stageManager.layoutCanvas);
			this.setDrawingSurfaceElement(this.application.ninja.stage.layoutCanvas);
			// clear just the 3d compass area
			this._drawingContext.save();
			this._drawingContext.rect(10, origTop-60, 100, 110);
			this._drawingContext.clip();

			this._drawingContext.lineWidth = 2.0;

			this._drawingContext.beginPath();
			this._drawingContext.strokeStyle = "red";
			this._drawingContext.moveTo(rO[0], rO[1]);
			this._drawingContext.lineTo(xO[0], xO[1]);
			this._drawingContext.closePath();
			this._drawingContext.stroke();
			this.drawArrowHead(rO, xO);

			this._drawingContext.beginPath();
			this._drawingContext.strokeStyle = "green";
			this._drawingContext.moveTo(rO[0], rO[1]);
			this._drawingContext.lineTo(yO[0], yO[1]);
			this._drawingContext.closePath();
			this._drawingContext.stroke();
			this.drawArrowHead(rO, yO);

			this._drawingContext.beginPath();
			this._drawingContext.strokeStyle = "blue";
			this._drawingContext.moveTo(rO[0], rO[1]);
			this._drawingContext.lineTo(zO[0], zO[1]);
			this._drawingContext.closePath();
			this._drawingContext.stroke();
			this.drawArrowHead(rO, zO);

			// restore the state
//			this.viewUtils.popViewportObj();
			this._drawingContext.restore();
			this.setDrawingSurfaceElement(saveContext);
			this._lineColor = saveColor;
			this._lineWidth = saveLineWidth;
			this._sourceSpaceElt = saveSource;
		}
	},

	drawArrowHead : {
		value: function(base, onAxis) {
			var headWidth = 6;

			// draw the arrowhead
			var head = MathUtils.interpolateLine3D( base, onAxis, 0.7 );
			p0 = head.slice(0);  p0[1] += headWidth;
			p1 = head.slice(0);  p1[0] += headWidth;
			p2 = head.slice(0);  p2[1] -= headWidth;
			p3 = head.slice(0);  p3[0] -= headWidth;

			this._drawingContext.beginPath();

			this._drawingContext.moveTo(base[0], base[1]);
			this._drawingContext.lineTo(onAxis[0], onAxis[1]);

			this._drawingContext.moveTo(onAxis[0], onAxis[1]);
			this._drawingContext.lineTo(p0[0], p0[1]);
			this._drawingContext.moveTo(onAxis[0], onAxis[1]);
			this._drawingContext.lineTo(p1[0], p1[1]);
			this._drawingContext.moveTo(onAxis[0], onAxis[1]);
			this._drawingContext.lineTo(p2[0], p2[1]);
			this._drawingContext.moveTo(onAxis[0], onAxis[1]);
			this._drawingContext.lineTo(p3[0], p3[1]);

			this._drawingContext.moveTo( p0[0], p0[1] );
			this._drawingContext.lineTo( p1[0], p1[1] );
			this._drawingContext.lineTo( p2[0], p2[1] );
			this._drawingContext.lineTo( p3[0], p3[1] );
			this._drawingContext.lineTo( p0[0], p0[1] );

			this._drawingContext.closePath();

			this._drawingContext.stroke();
		}
	},

	drawGridAxes : {
		value: function (ctr, width, height, headSize) {
			this._drawingContext.lineWidth = 2.0;

			this._lineColor = "red";
			var pt0 = ctr.slice(0), pt1 = pt0.slice(0);
			pt1[0] += width;
			this.moveTo(pt0);
			this.lineTo(pt1);
			var pt2 = pt1.slice(0);
			pt2[0] -= headSize;
			pt2[1] += headSize;
			this.lineTo(pt2);
			//this.moveTo(pt1);
			pt2[1] -= 2 * headSize;
			this.lineTo(pt2);
			this.lineTo(pt1);

			this._lineColor = "green";
			pt1 = pt0.slice(0);
			pt1[1] += height;
			this.moveTo(pt0);
			this.lineTo(pt1);
			pt2 = pt1.slice(0);
			pt2[1] -= headSize;
			pt2[0] += headSize;
			this.lineTo(pt2);
			//this.moveTo(pt1);
			pt2[0] -= 2 * headSize;
			this.lineTo(pt2);
			this.lineTo(pt1);

			this._lineColor = "blue";
			pt1 = pt0.slice(0);
			pt1[2] += height < width ? height : width;
			this.moveTo(pt0);
			this.lineTo(pt1);
			pt2 = pt1.slice(0);
			pt2[2] -= headSize;
			pt2[1] += headSize;
			this.lineTo(pt2);
			//this.moveTo(pt1);
			pt2[1] -= 2 * headSize;
			this.lineTo(pt2);
			this.lineTo(pt1);
		}
	}
});