/* <copyright>
Copyright (c) 2012, Motorola Mobility LLC.
All Rights Reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

* Redistributions of source code must retain the above copyright notice,
  this list of conditions and the following disclaimer.

* Redistributions in binary form must reproduce the above copyright notice,
  this list of conditions and the following disclaimer in the documentation
  and/or other materials provided with the distribution.

* Neither the name of Motorola Mobility LLC nor the names of its
  contributors may be used to endorse or promote products derived from this
  software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
</copyright> */

var Montage = require("montage/core/core").Montage,
    Component = require("montage/ui/component").Component,
    vecUtils = require("js/helper-classes/3D/vec-utils").VecUtils,
    Rectangle = require("js/helper-classes/3D/rectangle").Rectangle,
    ElementsMediator = require("js/mediators/element-mediator").ElementMediator;
///////////////////////////////////////////////////////////////////////
// Class ViewUtils
//      Viewing Utility functions
///////////////////////////////////////////////////////////////////////
exports.ViewUtils = Montage.create(Component, {
    ///////////////////////////////////////////////////////////////////////
    // Instance variables
    ///////////////////////////////////////////////////////////////////////

    m_viewportObj : { value: null},
    _perspectiveDist: { value: null},

    // keep a stack of viewport objects
    _viewportObjStack: { value: []},

    _rootElement: { value: null},
    _stageElement: { value: null},

    ///////////////////////////////////////////////////////////////////////
    // Property accessors
    ///////////////////////////////////////////////////////////////////////
    setViewportObj: {
        value: function( vp ) {
            this.m_viewportObj = vp;

            this._perspectiveDist = this.getPerspectiveDistFromElement( vp );
        }
    },
    getViewportObj: { value: function()                 {  return this.m_viewportObj;   } },

    setRootElement: { value: function( elt )            { this._rootElement = elt; } },
    getRootElement: { value: function ()                { return this._rootElement;        } },

    setStageElement: { value: function( elt )    {  this._stageElement = elt;    } },
    getStageElement: { value: function () { return this._stageElement; } },


    getPerspectiveDistance: { value: function () { return this._perspectiveDist; } },

    ///////////////////////////////////////////////////////////////////////
    // Camera and View Methods
    ///////////////////////////////////////////////////////////////////////
    getMatrixFromElement: {
        value: function( elt ) {
            var mat = ElementsMediator.getMatrix(elt);
            if(mat)
            {
                return mat;
            }
            else
            {
                return Matrix.I(4);
            }
        }
    },

    setMatrixForElement: {
        value: function( elt, mat, isChanging ) {
            ElementsMediator.setMatrix(elt, mat, isChanging);
        }
    },


    elementHas3D: {
        value: function( elt ) {
            return ElementsMediator.has3D(elt);
        }
    },

    getElementPlane: {
        value: function( elt ) {
            var bounds = this.getElementViewBounds3D( elt );
            var xArr = new Array(),  yArr = new Array(),  zArr = new Array();
            for (var j=0;  j<4;  j++)
            {
                var pt = this.localToGlobal( bounds[j],  elt );
                xArr.push(pt[0]);  yArr.push(pt[1]);  zArr.push(pt[2]);
            }
            var normal = MathUtils.getPolygonNormal( 4, xArr, yArr, zArr );
            //var d = -MathUtils.dot3( bounds[0], normal );
            var d = -MathUtils.dot3( [xArr[0],yArr[0],zArr[0]], normal );
            normal[3] = d;

            return normal;
        }
    },

    getUnprojectedElementPlane: {
        value: function( elt ) {
            var mat = this.getMatrixFromElement(elt);
            var plane = [mat[8],  mat[9],  mat[10],  mat[11]];

            // The translation value is a point on the plane
            this.pushViewportObj( elt );
            var ptOnPlane = this.getCenterOfProjection();
            this.popViewportObj();

            ptOnPlane[2] = 0;

            ptOnPlane = this.localToStageWorld(ptOnPlane, elt);
            plane[3] = -vecUtils.vecDot(3, plane, ptOnPlane );

            return plane;
        }
    },

    /*
     *  This method will return a normal to a plane containing the Z axis and either the
     *  x or y axis of the element.
     */
    getNormalToUnprojectedElementPlane:
    {
        value: function( elt, axis,  localMode )
        {
            var objMat = this.getMatrixFromElement(elt);
            var objMatInv = glmat4.inverse( objMat, [] );

            var xVec = [1,0,0];
            var yVec = [0,1,0];
            var zVec = [0,0,1];

            var stage = this.application.ninja.currentDocument.model.documentRoot;
            var stageMat = this.getMatrixFromElement(stage);

            var mat = glmat4.multiply( stageMat, objMat, [] );

            var viewDir;
            if (localMode)
            {
                var matInv = glmat4.inverse( mat, [] );
                viewDir = MathUtils.transformVector( [0,0,1], matInv );
            }
            else
            {
                var stageInv = glmat4.inverse( stageMat, [] );
                viewDir = MathUtils.transformVector( [0,0,1],  stageInv );
            }

            var plane;
            var xDot, yDot, zDot;
            switch (axis)
            {
                case 0:
                    yDot = Math.abs(vecUtils.vecDot(3, yVec, viewDir));
                    zDot = Math.abs(vecUtils.vecDot(3, zVec, viewDir));
                    if(yDot > zDot)
                        plane = vecUtils.vecCross( 3, zVec, xVec );
                    else
                        plane = vecUtils.vecCross( 3, yVec, xVec );
                    break;

                case 1:
                    xDot = Math.abs(vecUtils.vecDot(3, yVec, viewDir));
                    zDot = Math.abs(vecUtils.vecDot(3, zVec, viewDir));
                    if(xDot > zDot)
                        plane = vecUtils.vecCross( 3, zVec, yVec );
                    else
                        plane = vecUtils.vecCross( 3, xVec, yVec );
                    break;
                    break;

                case 2:
                    xDot = Math.abs(vecUtils.vecDot(3, xVec, viewDir));
                    yDot = Math.abs(vecUtils.vecDot(3, yVec, viewDir));

                    if(xDot > yDot)
                        plane = vecUtils.vecCross( 3, yVec, zVec );
                    else
                        plane = vecUtils.vecCross( 3, xVec, zVec );
                    break;
            }

            if (localMode)  plane = MathUtils.transformVector( plane, objMat );

            // The translation value is a point on the plane
            this.pushViewportObj( elt );
            var ptOnPlane = this.getCenterOfProjection();
            this.popViewportObj();

            ptOnPlane[2] = 0;

            ptOnPlane = this.localToStageWorld(ptOnPlane, elt);
            plane[3] = -vecUtils.vecDot(3, plane, ptOnPlane );

            return plane;
        }
    },

    getPerspectiveModeFromElement: {
        value: function( elt ) {
            return ElementsMediator.getPerspectiveMode(elt);
        }
    },

    getPerspectiveDistFromElement: {
        value: function( elt ) {
            var pDist = ElementsMediator.getPerspectiveDist(elt);
            if(pDist === "none") {
                return null;
            } else {
                return pDist;
            }
        }
    },

    getOffsetParent: {
        value: function( elt ) {
            var parent = elt.elementModel.getProperty("offsetParent");
            if(parent === "none") {
                return null;
            }
            else if(parent) {
                return parent;
            } else {
                parent = elt.offsetParent;
                if(parent) {
                    elt.elementModel.setProperty("offsetParent", parent);
                    return parent;
                } else {
                    elt.elementModel.setProperty("offsetParent", "none");
                    return null;
                }
            }
        }
    },

    getEyePoint: {
        value:function() {
            // this function should use the center of projection - it is currently hard wired to (0,0).
            var eye = [0, 0, this._perspectiveDist];

            return eye;
        }
    },

    getCameraMatrix: {
        value: function() {
            return this.getMatrixFromElement( this.m_viewportObj );
        }
    },

    getElementViewBounds3D: {
        value: function( elt ) {
            var bounds = this.getElementBounds( elt, true );
            var ptArray = new Array();
            for (var i=0;  i<4;  i++)
            {
                var pt = bounds.getPoint( i );
                pt[2] = 0; // z == 0
                ptArray.push( pt );
            }

            return ptArray;
        }
    },

    getElementBoundsInGlobal: {
        value: function (elt) {
            this.pushViewportObj(elt);
            var bounds3D = this.getElementViewBounds3D(elt);
            var tmpMat = this.getLocalToGlobalMatrix(elt);

            var zoomFactor = 1;
            var stage = this.application.ninja.stage;
            if (stage._viewport && stage._viewport.style && stage._viewport.style.zoom) {
                zoomFactor = Number(stage._viewport.style.zoom);
            }

            var sSL = stage._scrollLeft;
            var sST = stage._scrollTop;

            for (var j=0;  j<4;  j++) {
                var localPt = bounds3D[j];
                var tmpPt = this.localToGlobal2(localPt, tmpMat);

                if(zoomFactor !== 1) {
                    tmpPt = vecUtils.vecScale(3, tmpPt, zoomFactor);

                    tmpPt[0] += sSL*(zoomFactor - 1);
                    tmpPt[1] += sST*(zoomFactor - 1);
                }
                bounds3D[j] = tmpPt;
            }
            this.popViewportObj();
            return bounds3D;
        }
    },

    getCenterOfProjection: {
        value: function() {
            var cop;
            var vp = this.getViewportObj();
            if (vp)
            {
                var bounds = this.getViewportBounds();
                cop = bounds.getCenter();
                //cop = [bounds.getRight(), bounds.getBottom()];
            }

            return cop;
        }
    },

    preToPostScreen: {
        value: function( pt, elt ) {
            this.pushViewportObj( elt );
            var viewPt = this.screenToView( pt[0], pt[1], pt[2] );
            var mat = this.getMatrixFromElement( elt );
            var worldPt = MathUtils.transformPoint( viewPt, mat );
            var screenPt = this.viewToScreen( worldPt );
            this.popViewportObj();

            return screenPt;
        }
    },

    localToStageWorld: {
        value: function( localPt,  elt ) {
            this.pushViewportObj( elt );
            var viewPt = this.screenToView( localPt[0], localPt[1], localPt[2] );
            if ((elt == null) || (elt === this._stageElement))
            {
                this.popViewportObj();
                return viewPt;
            }
            var mat = this.getMatrixFromElement( elt );
            var worldPt = MathUtils.transformPoint( viewPt, mat );
            var stageWorldPt = this.postViewToStageWorld( worldPt, elt );
            this.popViewportObj();
            return stageWorldPt;
        }
    },

    // "post view" refers to a point in view space after the transform
    // i.e., pre and post view spaces.
    // this function is used by snapping routines to put element snap positions
    // into stage world space.
    postViewToStageWorld: {
        value: function( localPt, elt ) {
            if ((elt == null) || (elt === this._stageElement))  return localPt;

            // get the 3D transformation and 2D offset from the element
            var pt = localPt.slice(0);
            pt = MathUtils.makeDimension3( pt );

            // transform the point up the tree
            var child = elt;
            var parent = this.getOffsetParent(elt);
            while ( parent )
            {
                // go to screen space of the current child
                this.pushViewportObj( child );
                pt = this.viewToScreen( pt );
                this.popViewportObj();

                // to screen space of the parent
                var offset = this.getElementOffset( child );
                offset[2] = 0.0;
                pt = vecUtils.vecAdd( 3, pt, offset );

                // to view space of the parent
                this.pushViewportObj( parent );
                pt = this.screenToView( pt[0], pt[1], pt[2] );
                this.popViewportObj();

                // check if we are done
                if (parent === this._stageElement)  break;

                if (this.elementHas3D( parent ))
                {
                    var parentMat = this.getMatrixFromElement( parent );
                    pt = MathUtils.transformPoint( pt, parentMat );
                }

                child = parent;
                parent = this.getOffsetParent(parent);
            }

            return pt;
        }
    },

    localToGlobal: {
        value: function( localPt, elt ) {
            // get the 3D transformation and 2D offset from the element
            var pt = localPt.slice(0);
            if (pt.length < 3)  pt[2] = 0;
            if (pt.length == 4)  pt.pop();

            // transform the bounds up the tree
            var child = elt;
            while ( child )
            {
                pt = this.childToParent( pt, child );

//                if (child === this.application.ninja.currentDocument.model.documentRoot)  break;
//                child = child.offsetParent;

                if (child === this._stageElement)  break;
                if (child === this._rootElement)  break;
                child = this.getOffsetParent(child);
                if (child === this._rootElement)  break;
            }

            /////////////////////////////////////////////////////////
            // DEBUG CODE
            /*
             var tmpMat = this.getLocalToGlobalMatrix( elt );
             var hPt = localPt.slice(0);
             MathUtils.makeDimension4( hPt );
             var tmpPt = MathUtils.transformHomogeneousPoint( hPt, tmpMat );
             var gPt = MathUtils.applyHomogeneousCoordinate( tmpPt );
             */
            /////////////////////////////////////////////////////////

            return pt;
        }
    },

    localToGlobal2: {
        value: function( localPt, tmpMat ) {
            var hPt = localPt.slice(0);
            MathUtils.makeDimension4( hPt );
            var tmpPt = MathUtils.transformHomogeneousPoint( hPt, tmpMat );
            var gPt = MathUtils.applyHomogeneousCoordinate( tmpPt );
            gPt = MathUtils.makeDimension3( gPt );
            return gPt;
        }
    },

    childToParent: {
        value: function( pt, child ) {
            // pt should be a 3D (2D is ok) vector in the space of the element
            if (pt.length == 2)  pt[2] = 0;

            // transform the bounds up the tree
            var parent = this.getOffsetParent(child);
            // TODO - Should have a different way to check for new template mode
            if ( parent || (child === this.application.ninja.currentDocument.model.documentRoot) )
            {
                this.setViewportObj( child );

                // get the offset (previously computed
                var childMat = this.getMatrixFromElement( child );
                var offset = this.getElementOffset( child );
                offset[2] = 0;

                if (this.elementHas3D( child ))
                {
                    // TODO - Commenting out flatten support until new perspective workflow is fully working
                    // if (flatten)  pt[2] = 0;
//                    var flatten = (parent !== this._rootElement) && (ElementsMediator.getProperty(parent, "-webkit-transform-style") !== "preserve-3d");
//                    if(flatten)
//                    {
//                        pt[2] = 0;
//                    }
                    pt = this.screenToView( pt[0], pt[1], pt[2] );
                    pt[3] = 1;
                    //var wPt = childMat.multiply( pt );
                    var wPt = glmat4.multiplyVec3( childMat, pt, [] );
//                    if(flatten)
//                    {
//                        wPt[2] = 0;
//                    }
                    var scrPt = this.viewToScreen( wPt );
                    pt = scrPt;
                }

                //pt = pt.add(offset);
                pt = vecUtils.vecAdd(3, pt, offset);
            }

            return [pt[0], pt[1], pt[2]];
        }
    },


    viewToParent: {
        value: function( viewPt, child ) {
            // pt should be a 3D (2D is ok) vector in the space of the element
            var pt = viewPt.slice(0);
            if (pt.length == 2)  pt[2] = 0;
            pt[3] = 1;

            // transform the bounds up the tree
            var parent = this.getOffsetParent(child);
            if ( parent )
            {
                this.setViewportObj( child );

                // get the offset (previously computed
                var offset = this.getElementOffset( child );
                offset[2] = 0;
                pt = this.viewToScreen( pt );
                //pt = pt.add(offset);
                pt = vecUtils.vecAdd(3, pt, offset);
            }

            return [pt[0], pt[1], pt[2]];
        }
    },

    /**
     * The input plane is specified in stage world space.
     * The output plane is specified in the world space of the input element.
     **/
    globalPlaneToLocal: {
        value: function( plane,  elt ) {
            // get the four corners of the element in global space
            var bounds = this.getElementViewBounds3D( elt );
            var bounds3D = new Array();
            var stage = this.application.ninja.currentDocument.model.documentRoot;
            for (var i=0;  i<3;  i++)
            {
                var gPt = this.localToGlobal( bounds[i],  elt );
                bounds3D[i] = this.parentToChildWorld( gPt, stage );
            }

            /*
             this.pushViewportObj( elt );
             var parent = elt.offsetParent;
             var offset = this.getElementOffset( elt );
             offset[2] = 0;
             var localEyePt = this.getCenterOfProjection();
             localEyePt[2] = this.getPerspectiveDistance();
             localEyePt = vecUtils.vecAdd( 3, offset, localEyePt );
             var eyePt = this.localToGlobal( localEyePt, parent );
             eyePt = this.parentToChildWorld( eyePt, stage );
             */

            // drop the 4 corner points onto the plane and convert back to local space
            var ptArray = new Array;
            for (var i=0;  i<3;  i++)
            {
                var planePt = MathUtils.vecIntersectPlane( bounds3D[i], workingPlane, workingPlane );
                this.setViewportObj( stage );
                var viewPlanePt = this.viewToScreen( planePt );
                var globalPlanePt = this.localToGlobal( viewPlanePt, stage );
                var localPlanePt = this.globalToLocal( globalPlanePt, elt );
                ptArray.push( localPlanePt );
            }

            // get the plane from the 3 points
            var vec0 = vecUtils.vecSubtract(3, ptArray[0], ptArray[1] ),
                vec1 = vecUtils.vecSubtract(3, ptArray[2], ptArray[1] );
            var normal = MathUtils.cross( vec0, vec1 );
            var mag = vecUtils.vecMag(3, normal );
            if (MathUtils.fpSign(mag) == 0)
            {

            }
            var localPlane = vecUtils.vecNormalize( 3, normal, 1.0 );
            localPlane[3] = -vecUtils.vecDot( 3,  ptArray[0], localPlane );

        //        this.popViewportObj();

            return localPlane;
        }
    },

    parentToChild: {
        value: function( parentPt, child,  passthrough ) {
            var pt = parentPt.slice(0);
            if (pt.length == 2)  pt[2] = 0.0;

            // subtract off the the offset
            var offset = this.getElementOffset( child );
            offset[2] = 0.0;
            pt = vecUtils.vecSubtract( 3, pt, offset );

            // put the point in the view space of the child
            this.setViewportObj( child );
            var viewPt = this.screenToView( pt[0], pt[1], pt[2] );

            // find the plane to project to
            var mat = this.getMatrixFromElement( child );
            var plane = MathUtils.transformPlane( [0,0,1,0],  mat );

            // project the view point onto the plane
            var eyePt;
            if(this.getPerspectiveDistFromElement(child))
            {
                eyePt = this.getEyePoint();
            }
            else
            {
                eyePt = [viewPt[0], viewPt[1], 1400];
            }
            var projPt = MathUtils.vecIntersectPlane( eyePt, MathUtils.vecSubtract(viewPt,eyePt), plane );

            var childPt;
            if (passthrough)
            {
                //var inv = mat.inverse();
                var inv = glmat4.inverse( mat, []);
                var invPt = MathUtils.transformPoint( projPt, inv );

                // put into screen space (without projecting)
                childPt = this.viewToScreen( invPt );
            }
            else
            {
                childPt = this.viewToScreen( projPt );
            }

            return childPt;
        }
    },

    parentToChildWorld: {
        value: function( parentPt, child ) {
            var pt = parentPt.slice(0);
            if (pt.length == 2)  pt[2] = 0.0;

            // subtract off the the offset
            var offset = this.getElementOffset( child );
            offset[2] = 0.0;
            //pt = pt.subtract( offset );
            pt = vecUtils.vecSubtract(3, pt, offset);

            // put the point in the view space of the child
            this.pushViewportObj( child );
            var viewPt = this.screenToView( pt[0], pt[1], pt[2] );

            // find the plane to project to
            var mat = this.getMatrixFromElement( child );
            var plane = MathUtils.transformPlane( [0,0,1,0],  mat );

            // project the view point onto the plane
            var eyePt;
            if(this.getPerspectiveDistFromElement(child))
            {
                eyePt = this.getEyePoint();
            }
            else
            {
                eyePt = [viewPt[0], viewPt[1], 1400];
            }
            var projPt = MathUtils.vecIntersectPlane( eyePt, MathUtils.vecSubtract(viewPt,eyePt), plane );

            this.popViewportObj();

            return projPt;
        }
    },


    parentToChildVec: {
        value: function( parentPt, child, rtnEyePt ) {
            var pt = parentPt.slice(0);
            if (pt.length == 2)  pt[2] = 0.0;

            // subtract off the the offset
            var offset = this.getElementOffset( child );
            offset[2] = 0.0;
            pt = vecUtils.vecSubtract( 3, pt, offset );

            // put the point in the view space of the child
            this.setViewportObj( child );
            pt = this.screenToView( pt[0], pt[1], pt[2] );

            var eyePt;
            if(this.getPerspectiveDistFromElement(child))
            {
                eyePt = this.getEyePoint();
            }
            else
            {
                eyePt = [pt[0], pt[1], 1400];
            }
            var vec = vecUtils.vecSubtract(3, [pt[0], pt[1], pt[2]], eyePt);
            vec = vecUtils.vecNormalize( 3, vec );

            if(rtnEyePt)
            {
                rtnEyePt[0] = eyePt[0];
                rtnEyePt[1] = eyePt[1];
                rtnEyePt[2] = eyePt[2];
            }
            return vec;
        }
    },

    getElementBounds: {
        value: function( elt, localSpace ) {
            // optional argument localSpace, if true, puts the top left at (0,0).
            if (arguments.length < 2)  localSpace = false;

            var bounds, left, top, w, h;
            if(elt.elementModel.getProperty("offsetCache")) {
                left = elt.elementModel.getProperty("offsetLeft");
                top = elt.elementModel.getProperty("offsetTop");
                w = elt.elementModel.getProperty("offsetWidth");
                h = elt.elementModel.getProperty("offsetHeight");
            } else {
                left    = elt.offsetLeft;
                top     = elt.offsetTop;
                w       = elt.offsetWidth;
                h       = elt.offsetHeight;
                elt.elementModel.setProperty("offsetLeft", left);
                elt.elementModel.setProperty("offsetTop", top);
                elt.elementModel.setProperty("offsetWidth", w);
                elt.elementModel.setProperty("offsetHeight", h);
                elt.elementModel.setProperty("offsetCache", true);
            }



//            if (elt instanceof SVGSVGElement) {
            if(elt.nodeName.toLowerCase() === "svg") {
                        if(w instanceof SVGAnimatedLength)
                            w = w.animVal.value;
                        if(h instanceof SVGAnimatedLength)
                            h = h.animVal.value;
            }

            bounds = Object.create(Rectangle, {});
            if (localSpace)
            {
                left = 0;
                top = 0;
            }
            bounds.set( left, top,  w, h );

            return bounds;
        }
    },

    getViewportBounds: {
        value: function() {
            var bounds;
            var vp = this.m_viewportObj;
            if (vp)
            {
                bounds = this.getElementBounds( vp );
                bounds.setLeft(0);
                bounds.setTop(0);
            }

            return bounds;
        }
    },


    getElementOffset: {
        value: function( elt ) {
            var xOff, yOff, offset, parent, border, pS, bl, bt;

            if(elt.elementModel.getProperty("offsetCache")) {
                xOff = elt.elementModel.getProperty("offsetLeft");
                yOff = elt.elementModel.getProperty("offsetTop");
            } else {
                xOff    = elt.offsetLeft;
                yOff     = elt.offsetTop;
                elt.elementModel.setProperty("offsetLeft", xOff);
                elt.elementModel.setProperty("offsetTop", yOff);
                elt.elementModel.setProperty("offsetCache", true);
            }
            offset = [xOff, yOff];
            parent = this.getOffsetParent(elt);
            if(parent && (parent !== this._stageElement))
            {
                if(parent.elementModel.getProperty("border")) {
                    border = parent.elementModel.getProperty("border");
                    bl = parent.elementModel.getProperty("border-left-width");
                    bt = parent.elementModel.getProperty("border-top-width");
                } else {
                    if(elt.ownerDocument.defaultView) {
                        pS = elt.ownerDocument.defaultView.getComputedStyle(parent, null);
                        border = parseInt(pS.getPropertyValue("border"));
                        bl = parseInt(pS.getPropertyValue("border-left-width"));
                        bt = parseInt(pS.getPropertyValue("border-top-width"));
                        parent.elementModel.setProperty("border", border);
                        parent.elementModel.setProperty("border-left-width", bl);
                        parent.elementModel.setProperty("border-top-width", bt);
                    }
                }

                if(border) {
                    offset[0] += bl;
                    offset[1] += bt;
                }
            }

            if(elt === this._stageElement)
            {
                // TODO - Call a routine from the user document controller to get the offsets/margins
                // Once we expose the document controller to ViewUtils
                offset[0] += this.application.ninja.stage._userContentLeft;
                offset[1] += this.application.ninja.stage._userContentTop;
            }

            return offset;
        }
    },

    getCameraPos: {
        value: function() {
            var cameraPos = [0, 0, this._perspectiveDist];
            return cameraPos;
        }
    },

    getZIndex: {
        value: function( elt ) {
            var zIndex = 0;
            if (elt.style.zIndex)  zIndex = Number( elt.style.zIndex );

            return zIndex;
        }
    },

    setZIndex: {
        value: function( elt, zIndex ) {
            elt.style.zIndex = zIndex;
        }
    },


    screenToView: {
        value: function(xScr, yScr, zScr) {
            if (arguments.length < 3)  zScr = 0;

              var ctr = this.getCenterOfProjection();
              var xCtr = ctr[0],  yCtr = ctr[1];
    //        var bounds = this.getViewportBounds();
    //        var xCtr    = bounds.getLeft() + 0.5*bounds.getWidth(),
    //            yCtr    = bounds.getTop()  + 0.5*bounds.getHeight();

            // perspective origin not supported
            /*
            if (viewportObj.style.webkit-perspective-origin)
            {
            }
            */

            //var yView = yCtr - yScr,
            var yView = yScr - yCtr,
                xView = xScr - xCtr,
                zView = zScr;

            return [xView, yView, zView];
        }
    },

    viewToScreen: {
        value: function( viewPoint ) {
            var scrPt;
            var viewport = this.m_viewportObj;
            if (viewport)
            {
                // project the point to the z=0 plane
                var viewPt = this.projectToViewPlane( viewPoint );

                // convert from view to screen space
    //            var bounds = this.getViewportBounds();
    //            var ctr = bounds.getCenter();
                var ctr = this.getCenterOfProjection();
                scrPt = [ctr[0] + viewPt[0],  ctr[1] + viewPt[1],  viewPt[2]];
            }

            return scrPt;
        }
    },


    projectToViewPlane: {
        value: function( viewPos ) {
            if(!this._perspectiveDist)
            {
                return viewPos.slice(0);
            }
            var viewPt;
            var viewport = this.m_viewportObj;
            if (viewport)
            {
                viewPt = [viewPos[0], viewPos[1], viewPos[2]];

                // apply the perspective
                var dist = this._perspectiveDist - viewPt[2];
                var scale = 0.0;
                if (MathUtils.fpSign(dist) != 0)
                {
                    scale = this._perspectiveDist / dist;
                    viewPt[0] *= scale;
                    viewPt[1] *= scale;
                    viewPt[2] *= scale;
                }
                else
                    console.log( "***** infinite projection  *****" );
            }

            return viewPt;
        }
    },


    unproject: {
        value: function( pt ) {
            if(!this._perspectiveDist)
            {
                return pt.slice(0);
            }
            var viewPt;
            var viewport = this.m_viewportObj;
            if (viewport)
            {
                viewPt = pt.slice(0);
                MathUtils.makeDimension3( viewPt );

                // calculate the unprojected Z value
                var p = this._perspectiveDist;
                var zp = viewPt[2];        // zp == z projected
                var z = zp*p/(zp + p);        // z  == unprojected z value

                var s = (p - z)/p;
                var x = viewPt[0] * s,
                    y = viewPt[1] * s;

                viewPt[0] = x;
                viewPt[1] = y;
                viewPt[2] = z;
            }

            return viewPt;
        }
    },

    worldToView: {
        value: function( worldPos )    {
            // we have no camera, always the same point
            return worldPos;
        }
    },

    viewToWorld: {
        value: function( viewPos ) {
            // we have no camera, always the same point
            return viewPos;
        }
    },

    worldToScreen: {
        value: function( worldPos )    {
            var scrPt;
            var viewport = this.m_viewportObj;
            if (viewport)
            {
                var viewPt = this.worldToView( worldPos )
                if (viewPt)
                    scrPt = this.viewToScreen( viewPt );
            }

            return scrPt;
        }
    },

    getStageWorldToGlobalMatrix:
    {
        value: function()
        {
            var stage = this.application.ninja.currentDocument.model.documentRoot;

            this.pushViewportObj( stage );
            // put the point into screen space of the stage - requires
            // a translation to the top/left only
            var cop = this.getCenterOfProjection();
            var v2s = Matrix.Translation([cop[0], cop[1], 0]);
            this.popViewportObj();

            // append the localToGlobal matrix of the stage.
            var mat = this.getLocalToGlobalMatrix( stage );
            glmat4.multiply( mat, v2s );
            return mat;
        }
    },

    localScreenToLocalWorld: {
        value: function( objPt,  elt ) {
            MathUtils.makeDimension3( objPt );
            this.pushViewportObj( elt );
            var viewPt = this.screenToView( objPt[0], objPt[1], objPt[2] );
            this.popViewportObj();
            var mat = this.getMatrixFromElement( elt );
            viewPt = MathUtils.transformPoint( viewPt, mat );
            return viewPt;
        }
    },

    globalScreenToLocalWorld: {
        value: function( globalPt,  elt ) {
            var objPt = this.globalToLocal( globalPt, elt );
            var viewPt = this.localScreenToLocalWorld( objPt,  elt );

            /*
            MathUtils.makeDimension3( objPt );
            this.pushViewportObj( elt );
            var viewPt = this.screenToView( objPt[0], objPt[1], objPt[2] );
            this.popViewportObj();
            var mat = this.getMatrixFromElement( elt );
            viewPt = MathUtils.transformPoint( viewPt, mat );
            */

            return viewPt;
        }
    },

    globalToLocal: {
        value: function( targetScrPt, elt )    {
            var objPt;

            // get matrix going from object local to screen space, and it's inverse
            var o2w = this.getLocalToGlobalMatrix( elt );
            //var w2o = o2w.inverse();
            var w2o = glmat4.inverse( o2w, []);

            // transform the input point in screen space to object space
            var tmpInPt = targetScrPt.slice(0);
            tmpInPt = MathUtils.makeDimension4( tmpInPt );
            tmpInPt[2] = 0.0;    // z == 0
            var tmpPt1 = MathUtils.transformHomogeneousPoint( tmpInPt, w2o);
            tmpPt1 = MathUtils.applyHomogeneousCoordinate( tmpPt1 );

            // get a second point in object space starting from the input point plus an (arbitrary) z offset
            tmpInPt[2] = 100.0;
            var tmpPt2 = MathUtils.transformHomogeneousPoint( tmpInPt, w2o);
            tmpPt2 = MathUtils.applyHomogeneousCoordinate( tmpPt2 );

            // project the 2 object space points onto the original plane of the object
            objPt = MathUtils.vecIntersectPlane( tmpPt1, vecUtils.vecSubtract(3, tmpPt2, tmpPt1), [0,0,1,0] );

            return objPt;
        }
    },

    getLocalToGlobalMatrix: {
        value: function( elt ) {
            var mat = Matrix.I(4),
                projMat,
                pDist;
            // TODO - Commenting out flatten support until new perspective workflow is fully working
            var zMat = Matrix.I(4);
//            zMat[9] = 0;
//            zMat[10] = 0;
//            zMat[11] = 0;
//            zMat[12] = 0;
            while (elt)
            {
                this.pushViewportObj( elt );
                    var cop = this.getCenterOfProjection();
                    var s2v = Matrix.Translation([-cop[0], -cop[1], 0]);
                    var objMat = this.getMatrixFromElement( elt );

                    //var projMat = Matrix.I(4).multiply( this.getPerspectiveDistFromElement(elt) );
                    pDist = this.getPerspectiveDistFromElement(elt);
                    if(pDist)
                    {
                        projMat = glmat4.scale(Matrix.I(4), [pDist,pDist,pDist], []);
                        projMat[11] = -1;
                        projMat[15] = 1400;
                    }
                    var v2s = Matrix.Translation([cop[0], cop[1], 0]);

                    glmat4.multiply( s2v, mat, mat );
                    glmat4.multiply( objMat, mat, mat );
//                    glmat4.multiply( projMat, mat, mat );
                    if(pDist)
                    {
                        //mat = projMat.multiply( mat );
                        glmat4.multiply( projMat, mat, mat );
                        pDist = null;
                    }
                    glmat4.multiply( v2s, mat, mat );

                // TODO - Commenting out flatten support until new perspective workflow is fully working
//                    var flatten = (elt !== this._rootElement) && (elt.parentElement !== this._rootElement) && (ElementsMediator.getProperty(elt.parentElement, "-webkit-transform-style") !== "preserve-3d");
//                    if(flatten)
//                    {
//                        glmat4.multiply( zMat, mat, mat );
//                    }

                    // offset to the parent
                    var offset = this.getElementOffset( elt );
                    var offMat = Matrix.Translation([offset[0], offset[1], 0]);
                    //mat = offMat.multiply( mat );
                    glmat4.multiply( offMat, mat, mat );

                this.popViewportObj();

                if (elt === this._stageElement)  break;
                if (elt === this._rootElement)  break;
                elt = this.getOffsetParent(elt);
                if (elt === this._rootElement)  break;
            }

            return mat;
        }
    },

    getObjToStageWorldMatrix: {
        value: function( elt, shouldProject ) {
            var mat = Matrix.I(4),
                projMat,
                pDist;
            while (elt)
            {
                this.pushViewportObj( elt );
                    var cop = this.getCenterOfProjection();
                    var s2v = Matrix.Translation([-cop[0], -cop[1], 0]);
                    var objMat = this.getMatrixFromElement( elt );
                    if(shouldProject)
                    {
                        pDist = this.getPerspectiveDistFromElement(elt);
                        if(pDist)
                        {
                            projMat = glmat4.scale(Matrix.I(4), [pDist,pDist,pDist], []);
                            projMat[11] = -1;
                            projMat[15] = 1400;
                        }
                    }
                    var v2s = Matrix.Translation([cop[0], cop[1], 0]);
                this.popViewportObj();

                // multiply all the matrices together
                //mat = s2v.multiply( mat );
                glmat4.multiply( s2v, mat, mat );
                if (elt === this._stageElement)  break;
                //mat = objMat.multiply( mat );
                glmat4.multiply( objMat, mat, mat );
                if(shouldProject && pDist)
                {
                    //mat = projMat.multiply( mat );
                    glmat4.multiply( projMat, mat, mat );
                    pDist = null;
                }
                //mat = v2s.multiply( mat );
                glmat4.multiply( v2s, mat, mat );

                // offset to the parent
                var offset = this.getElementOffset( elt );
                var offMat = Matrix.Translation([offset[0], offset[1], 0]);
                //mat = offMat.multiply( mat );
                glmat4.multiply( offMat, mat, mat );

                elt = this.getOffsetParent(elt);
            }

            return mat;
        }
    },

    getLocalToStageWorldMatrix: {
        value: function( elt, shouldProject, shouldLocalTransform ) {
            var mat = Matrix.I(4);
            while (elt)
            {
                this.pushViewportObj( elt );
                    var cop = this.getCenterOfProjection();
                    var s2v = Matrix.Translation([-cop[0], -cop[1], 0]);
                    var objMat = this.getMatrixFromElement( elt );
                    var projMat;
                    if(shouldProject)
                    {
                        //projMat = Matrix.I(4).multiply( this.getPerspectiveDistFromElement(elt) );
                        var pDist = this.getPerspectiveDistFromElement(elt);
                        var projMat = glmat4.scale(Matrix.I(4), [pDist,pDist,pDist], []);
                        projMat[11] = -1;
                        projMat[15] = 1400;
                    }
                    var v2s = Matrix.Translation([cop[0], cop[1], 0]);
                this.popViewportObj();

                // multiply all the matrices together
                //mat = s2v.multiply( mat );
                glmat4.multiply( s2v, mat, mat );
                if (elt === this._stageElement)  break;
                //mat = objMat.multiply( mat );
                if (shouldLocalTransform) {
                    glmat4.multiply( objMat, mat, mat );
                }
                if(shouldProject)
                {
                    //mat = projMat.multiply( mat );
                    glmat4.multiply( projMat, mat, mat );
                }
                //mat = v2s.multiply( mat );
                glmat4.multiply( v2s, mat, mat );

                // offset to the parent
                var offset = this.getElementOffset( elt );
                var offMat = Matrix.Translation([offset[0], offset[1], 0]);
                //mat = offMat.multiply( mat );
                glmat4.multiply( offMat, mat, mat );

                elt = elt.parentElement;
            }

            return mat;
        }
    },

    getUpVectorFromMatrix: {
        value: function( mat )    {
            //var inv = mat.inverse();
            var yAxis = [mat[4],  mat[5], mat[6]];
            yAxis = vecUtils.vecNormalize( 3, yAxis );
            return yAxis;
        }
    },

    getRollVectorFromMatrix: {
        value: function( mat )    {
            //var inv = mat.inverse();
            var zAxis = [mat[8],  mat[9], mat[10]];
            zAxis = vecUtils.vecNormalize( 3, zAxis );
            return zAxis;
        }
    },

    getMatrixFromVectors: {
        value: function( upVec, zVec )    {
            // get the set of 3 orthogonal axes
            var yAxis = upVec.slice(0);
            MathUtils.makeDimension3( yAxis );
            yAxis = vecUtils.vecNormalize( 3, yAxis );

            var zAxis = zVec.slice(0);
            MathUtils.makeDimension3( zAxis );
            zAxis = vecUtils.vecNormalize( 3, zAxis );
            var xAxis = MathUtils.cross( yAxis, zAxis );

            var dot = MathUtils.dot3( yAxis, zAxis );
            if (MathUtils.fpSign(dot) != 0)
                console.log( "axes not orthogonal" );

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

            return mat;
        }
    },

    createMatrix: {
        value: function( startMat, startMatInv,  ctr, upVec,  zAxis,  azimuth, altitude )    {
            //console.log( "upVec: " + upVec + ", zAxis: " + zAxis + ", azimuth: " + azimuth + ", altitude: " + altitude );

            var yMat  = Matrix.RotationY( azimuth ),
                xMat  = Matrix.RotationX( altitude );
            //mat = yMat.multiply( xMat );
            var mat = glmat4.multiply( yMat, xMat, []);

            // apply the 'up' matrix
            var upMat = this.getMatrixFromVectors( upVec,  zAxis );
            //this.checkMat( upMat );
            //mat = upMat.multiply( mat );
            glmat4.multiply( upMat, mat, mat );

            // apply the start matrix
            //mat = mat.multiply( startMatInv );
            //mat = startMat.multiply( mat );

            if(ctr)
            {
                // pre-translate by the negative of the transformation center
                var tMat = Matrix.I(4);
                tMat[12] = -ctr[0];
                tMat[13] = -ctr[1];
                tMat[14] = -ctr[2];
                //mat  = mat.multiply( tMat );
                glmat4.multiply( mat, tMat );

                // translate back to the transform center
                tMat[12] = ctr[0];
                tMat[13] = ctr[1];
                tMat[14] = ctr[2];
                //mat  = tMat.multiply( mat );
                glmat4.multiply( tMat, mat, mat );
            }

            //this.checkMat( mat );
            //this.printMat( mat );

            return mat;
        }
    },

    printMat: {
        value: function( mat )    {
            console.log( "\tmat: " + mat );
    //        console.log( "\t\t" + mat[0] );
    //        console.log( "\t\t" + mat[1] );
    //        console.log( "\t\t" + mat[2] );
        }
    },

    checkMat: {
        value: function( mat )    {
            var    xAxis = [mat[0],  mat[1],  mat[ 2]],
                yAxis = [mat[4],  mat[5],  mat[ 6]],
                zAxis = [mat[8],  mat[9],  mat[10]];

            var xMag = MathUtils.vecMag3( xAxis ),  yMag = MathUtils.vecMag3( yAxis ),  zMag = MathUtils.vecMag3( zAxis );
            if (MathUtils.fpCmp(xMag,1) != 0)
                console.log( "xAxis not unit length: " + xMag );
            if (MathUtils.fpCmp(yMag,1) != 0)
                console.log( "yAxis not unit length: " + yMag );
            if (MathUtils.fpCmp(zMag,1) != 0)
                console.log( "zAxis not unit length: " + zMag );

            /*
            var dot = MathUtils.dot3( xAxis, yAxis );
            if (MathUtils.fpSign(dot) != 0)
                console.log( "X-Y not orthogonal" );

            dot = MathUtils.dot3( xAxis, zAxis );
            if (MathUtils.fpSign(dot) != 0)
                console.log( "X-Z not orthogonal" );

            dot = MathUtils.dot3( yAxis, zAxis );
            if (MathUtils.fpSign(dot) != 0)
                console.log( "Y-Z not orthogonal" );
            */
        }
    },

    pushViewportObj: {
        value: function( obj )    {
            this.setViewportObj(obj);
            this._viewportObjStack.push( obj );
        }
    },

    popViewportObj: {
        value: function()    {
            if (this._viewportObjStack.length == 0)
            {
                throw( "viewport object stack underflow" );
                return;
            }

            var rtn = this.m_viewportObj;
            this.setViewportObj(this._viewportObjStack.pop());
            return rtn;
        }
    },

///////////////////////////////////////////////////////////////////////////////////
// Montage update map
//
//  NO LONGER SUPPORTED:
//      stageManagerModule
//      drawLayoutModule
//
//  STAGE ACCESSORS:
//  activeDocument:                 this.application.ninja.currentDocument
//  userContent (stage):            this.application.ninja.currentDocument.model.documentRoot
//  stageManager:                   this.application.ninja.stage                                // MainApp\js\stage\stage.reel\stage.js
//  stageManager._canvas:           this.application.ninja.stage.canvas
//  stageManager.layoutCanvas:      this.application.ninja.stage.layoutCanvas
//  stageManager.drawingCanvas:     this.application.ninja.stage.drawingCanvas
//  viewUtils:                      stage.viewUtils;
//  snapManager                     stage.snapManager;
//
//  REDRAW FUNCTIONS
//  window.stageManager.drawSelectionRec(true):         this.application.ninja.stage.updateStage = true;
//  drawLayoutModule.drawLayout.redrawDocument()                            OR
//  window.stageManager.drawSelectionRec(true)          this.getStage().draw();
//  drawLayoutModule.drawLayout.redrawDocument();

//  SELECTION MANAGER
//  selected elements:              this.application.ninja.selectedElements
//  selectionManager                this.application.ninja.selectionController
//  selected elements:              this.application.ninja.selectionController.selectElements
//
//  MISCELLANEOUS
//  event.layerX/Y:                 var pt = viewUtils.getMousePoint(event);

    getStage: {
        value: function()
        {
            return this.application.ninja.stage.snapManager.getStage();
        }
    },

    clearStageTranslation: {
        value: function() {
            if (this.application.ninja.currentDocument)
            {
                // get the user content object
                var userContent = this.application.ninja.currentDocument.model.documentRoot;
                if (!userContent)  return;
                this.setViewportObj( userContent );

                // calculate the new matrix
                var ucMat = this.getMatrixFromElement(userContent);
                var targetMat = ucMat.slice();
                targetMat[12] = 0;  targetMat[13] = 0;  targetMat[14] = 0;
                var ucMatInv = glmat4.inverse( ucMat, [] );
                var deltaMat = glmat4.multiply( targetMat, ucMatInv, [] );
                this.setMatrixForElement(userContent, targetMat );
            }
        }
    },

    setStageZoom: {
        value:function( globalPt,  zoomFactor ) {
            var localPt;
            var tmp1, tmp2, tmp3;

            if (this.application.ninja.currentDocument)
            {
                var userContent = this.application.ninja.currentDocument.model.documentRoot;
                if (!userContent)  return;
                this.setViewportObj( userContent );
                var userContentMat = this.getMatrixFromElement(userContent);

                // create a matrix to do the scaling
                // the input zoomFactor is the total zoom for the resulting matrix, so we need to
                // get the current scale from the existing userContent matrix
                // we assume a uniform scale.
                var ucX = [userContentMat[0], userContentMat[1+0], userContentMat[2+0]];
                var ucY = [userContentMat[4], userContentMat[1+4], userContentMat[2+4]];
                var ucZ = [userContentMat[8], userContentMat[1+8], userContentMat[2+8]];
                var sx = vecUtils.vecMag(3, ucX),
                    sy = vecUtils.vecMag(3, ucY),
                    sz = vecUtils.vecMag(3, ucZ);
                if ((MathUtils.fpCmp(sx,sy) != 0) || (MathUtils.fpCmp(sx,sz)) != 0)
                    console.log( "**** non-uniform scale in view matrix **** " + sx + ", " + sy + ", " + sz );
                var newZoomFactor = zoomFactor/sx;
//                console.log( "old zoom: " + zoomFactor + ", new zoom: " + newZoomFactor );
                if (MathUtils.fpCmp(newZoomFactor,1.0) == 0)
                    console.log( "no zoom applied" );
                else
                {
                    var zoomMat=[newZoomFactor,0,0,0,0,newZoomFactor,0,0,0,0,newZoomFactor,0,0,0,0,1];

                    // get  a point in local userContent space
                    localPt  = this.globalToLocal( globalPt, userContent );
                    var scrPt = this.screenToView( localPt[0], localPt[1], localPt[2] );
                    var worldPt  = MathUtils.transformPoint( scrPt, userContentMat );
                    tmp1 = this.localToGlobal( localPt,  userContent ); // DEBUG - remove this line

                    // set the viewport object
                    this.setViewportObj( userContent );

                    // scale around the world point to give the same screen location
                    var transPt = worldPt.slice();
                    var transPtNeg = transPt.slice();
                    vecUtils.vecNegate( 3, transPtNeg );
                    var trans1 = Matrix.Translation( transPtNeg ),
                        trans2 = Matrix.Translation( transPt );
                    var mat = glmat4.multiply( zoomMat, trans1, [] );
                    glmat4.multiply( trans2, mat, mat );
                    var newUCMat = glmat4.multiply( mat, userContentMat, []);

                    this.setMatrixForElement(userContent, newUCMat );
                    tmp2 = this.localToGlobal( localPt,  userContent ); // DEBUG - remove this line

                    // apply to the stage background
//                  var stageBG = this.application.ninja.currentDocument.stageBG;
//                  var stageBGMat = this.getMatrixFromElement(stageBG);
//                  var newStageBGMat = glmat4.multiply( mat, stageBGMat, []);
//                  this.setMatrixForElement(stageBG, newStageBGMat );
                }
            }
        }
    },

    getCanvas:
    {
        value: function()
        {
            return this.application.ninja.stage.canvas;
        }
    },

    getSelectionManager:
    {
        value: function()
        {
            return this.application.ninja.selectionController;
        }

    },

    getSelectedElements:
    {
        value: function()
        {
            return this.application.ninja.selectedElements.slice();
        }
    },

    getMousePoint:
    {
        value: function(event)
        {
            var point = webkitConvertPointFromPageToNode(this.getCanvas(), new WebKitPoint(event.pageX, event.pageY));
            return [point.x, point.y];
        }
    }

///////////////////////////////////////////////////////////////////////////////////

});