/* <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> */

var GeomObj = require("js/lib/geom/geom-obj").GeomObj;
var ShapePrimitive =    require("js/lib/geom/shape-primitive").ShapePrimitive;
var MaterialsModel = require("js/models/materials-model").MaterialsModel;

 ///////////////////////////////////////////////////////////////////////
// Class GLRectangle
//      GL representation of a rectangle.
//      Derived from class GeomObj
///////////////////////////////////////////////////////////////////////
exports.Rectangle = Object.create(GeomObj, {
	// CONSTANTS
	N_TRIANGLES: { value : 15, writable: false },       // TODO - This is not being used anywhere. Remove?

	///////////////////////////////////////////////////////////////////////
	// Instance variables
	///////////////////////////////////////////////////////////////////////
    _width: { value : 2.0, writable: true },
    _height: { value : 2.0, writable: true },
    _xOffset: { value : 0, writable: true },
    _yOffset: { value : 0, writable: true },

    _tlRadius: { value : 0, writable: true },
    _trRadius: { value : 0, writable: true },
    _blRadius: { value : 0, writable: true },
    _brRadius: { value : 0, writable: true },

    _strokeWidth: { value : 0.25, writable: true },
    _strokeStyle: { value : "Solid", writable: true },

    init: {
        value: function(world, xOffset, yOffset, width, height, strokeSize, strokeColor, fillColor,
                      tlRadius, trRadius, blRadius, brRadius, strokeMaterial, fillMaterial, strokeStyle) {
            this.m_world = world;

            if (arguments.length > 0) {
                this._width = width;
                this._height = height;
                this._xOffset = xOffset;
                this._yOffset = yOffset;

                this._strokeWidth = strokeSize;
                this._strokeColor = strokeColor;
                this._fillColor = fillColor;

                this.setTLRadius(tlRadius);
                this.setTRRadius(trRadius);
                this.setBLRadius(blRadius);
                this.setBRRadius(brRadius);

                this._strokeStyle = strokeStyle;

			this._matrix = Matrix.I(4);
            }

            // the overall radius includes the fill and the stroke.  separate the two based on the stroke width
            //  this._fillRad = this._radius - this._strokeWidth;
            //    var err = 0.05;
            var err = 0;
            this._fillWidth = this._width - this._strokeWidth  + err;
            this._fillHeight = this._height - this._strokeWidth + err;

            this._materialAmbient  = [0.2, 0.2, 0.2,  1.0];
            this._materialDiffuse  = [0.4, 0.4, 0.4,  1.0];
            this._materialSpecular = [0.4, 0.4, 0.4,  1.0];

            if(strokeMaterial) {
                this._strokeMaterial = strokeMaterial;
            } else {
                this._strokeMaterial = MaterialsModel.getMaterial( MaterialsModel.getDefaultMaterialName() );
            }

            if(fillMaterial) {
                this._fillMaterial = fillMaterial;
            } else {
                this._fillMaterial = MaterialsModel.getMaterial( MaterialsModel.getDefaultMaterialName() );
            }
        }
    },

	///////////////////////////////////////////////////////////////////////
    // Property Accessors
    ///////////////////////////////////////////////////////////////////////
    // TODO - Use getters/setters in the future
    getStrokeWidth: {
        value: function() {
            return this._strokeWidth;
        }
    },

    setStrokeWidth: {
        value: function(w) {
            this._strokeWidth = w;
        }
    },

    getStrokeMaterial: {
        value: function() {
            return this._strokeMaterial;
        }
    },

    setStrokeMaterial: {
        value: function(m) {
            this._strokeMaterial = m;
        }
    },

    getFillMaterial: {
        value: function() {
            return this._fillMaterial;
        }
    },

    setFillMaterial: {
        value: function(m) {
            this._fillMaterial = m;
        }
    },
	///////////////////////////////////////////////////////////////////////
    // update the "color of the material
    getFillColor: {
        value: function() {
            return this._fillColor;
        }
    },

//    setFillColor: {
//        value: function(c) {
//            this._fillColor = c;
//        }
//    },
    getStrokeColor: {
        value: function() {
            return this._strokeColor;
        }
    },

//    setStrokeColor: {
//        value: function(c) {
//            this._strokeColor = c;
//        }
//    },
    ///////////////////////////////////////////////////////////////////////
    getTLRadius: {
        value: function() {
            return this._tlRadius;
        }
    },

    setTLRadius: {
        value: function(r) {
            this._tlRadius = Math.min(r, (this._height - this._strokeWidth)/2, (this._width - this._strokeWidth)/2);
        }
    },

    getTRRadius: {
        value: function() {
            return this._trRadius;
        }
    },

    setTRRadius: {
        value: function(r) {
            this._trRadius = Math.min(r, (this._height - this._strokeWidth)/2, (this._width - this._strokeWidth)/2);
        }
    },

    getBLRadius: {
        value: function() {
            return this._blRadius;
        }
    },

    setBLRadius: {
        value: function(r) {
            this._blRadius = Math.min(r, (this._height - this._strokeWidth)/2, (this._width - this._strokeWidth)/2);
        }
    },

    getBRRadius: {
        value: function() {
            return this._brRadius;
        }
    },

    setBRRadius: {
        value: function(r) {
            this._brRadius = Math.min(r, (this._height - this._strokeWidth)/2, (this._width - this._strokeWidth)/2);
        }
    },

    getStrokeStyle: {
        value: function() {
            return this._strokeStyle;
        }
    },

    setStrokeStyle: {
        value: function(s) {
            this._strokeStyle = s;
        }
    },

    getWidth: {
        value: function() {
            return this._width;
        }
    },

    setWidth: {
        value: function(w) {
            this._width = w;
        }
    },

    getHeight: {
        value: function() {
            return this._height;
        }
    },

    setHeight: {
        value: function(h) {
            this._height = h;
        }
    },

    geomType: {
        value: function() {
            return this.GEOM_TYPE_RECTANGLE;
        }
    },

	///////////////////////////////////////////////////////////////////////
	// Methods
	///////////////////////////////////////////////////////////////////////
	// JSON export
    exportJSON: {
        value: function() {
            var jObj =
            {
                'type'			: this.geomType(),
                'xoff'			: this._xOffset,
                'yoff'			: this._yOffset,
                'width'			: this._width,
                'height'		: this._height,
                'strokeWidth'	: this._strokeWidth,
                'strokeColor'	: this._strokeColor,
                'fillColor'		: this._fillColor,
                'tlRadius'		: this._tlRadius,
                'trRadius'		: this._trRadius,
                'blRadius'		: this._blRadius,
                'brRadius'		: this._brRadius,
                'innerRadius'	: this._innerRadius,
                'strokeStyle'	: this._strokeStyle,
                'strokeMat'		: this._strokeMaterial ? this._strokeMaterial.getName() :  MaterialsModel.getDefaultMaterialName(),
                'fillMat'		: this._fillMaterial ?  this._fillMaterial.getName() :  MaterialsModel.getDefaultMaterialName(),
                'materials'		: this.exportMaterialsJSON()
            };

            return jObj;
        }
    },

    importJSON: {
        value: function(jObj) {
            this._xOffset			= jObj.xoff;
            this._yOffset			= jObj.yoff;
            this._width				= jObj.width;
            this._height			= jObj.height;
            this._strokeWidth		= jObj.strokeWidth;
            this._strokeColor		= jObj.strokeColor;
            this._fillColor			= jObj.fillColor;
            this._tlRadius			= jObj.tlRadius;
            this._trRadius			= jObj.trRadius;
            this._blRadius			= jObj.blRadius;
            this._brRadius			= jObj.brRadius;
            this._innerRadius		= jObj.innerRadius;
            this._strokeStyle		= jObj.strokeStyle;
            var strokeMaterialName	= jObj.strokeMat;
            var fillMaterialName	= jObj.fillMat;

            var strokeMat = MaterialsModel.getMaterial( strokeMaterialName );
            if (!strokeMat) {
                console.log( "object material not found in library: " + strokeMaterialName );
                strokeMat = MaterialsModel.getMaterial(  MaterialsModel.getDefaultMaterialName() );
            }
            this._strokeMaterial = strokeMat;

            var fillMat = MaterialsModel.getMaterial( fillMaterialName );
            if (!fillMat) {
                console.log( "object material not found in library: " + fillMaterialName );
                fillMat = MaterialsModel.getMaterial(  MaterialsModel.getDefaultMaterialName() );
            }
            this._fillMaterial = fillMat;

            this.importMaterialsJSON( jObj.materials );
        }
    },

    buildBuffers: {
        value: function() {
            // get the world
            var world = this.getWorld();
            if (!world)  throw( "null world in buildBuffers" );
            //console.log( "GLRectangle.buildBuffers " + world._worldCount );
            if (!world._useWebGL)  return;

            // make sure RDGE has the correct context
            RDGE.globals.engine.setContext( world.getCanvas().rdgeid );

            // create the gl buffer
            var gl = world.getGLContext();

            var tlRadius = this._tlRadius; //top-left radius
            var trRadius = this._trRadius;
            var blRadius = this._blRadius;
            var brRadius = this._brRadius;

            // declare the arrays to hold the parts
            this._primArray = [];
            this._materialArray = [];
            this._materialTypeArray = [];
            this._materialNodeArray = [];

            // get the normalized device coordinates (NDC) for
            // all position and dimensions.
            var vpw = world.getViewportWidth(),  vph = world.getViewportHeight();
            var xNDC = 2*this._xOffset/vpw,  yNDC = 2*this._yOffset/vph,
                xFillNDC = this._width/vpw,  yFillNDC = this._height/vph,
                strokeSizeNDC = 2*this._strokeWidth/vpw,
                tlRadiusNDC = 2*tlRadius/vpw,  yTLRadiusNDC = 2*tlRadius/vph,
                trRadiusNDC = 2*trRadius/vpw,  yTRRadiusNDC = 2*trRadius/vph,
                blRadiusNDC = 2*blRadius/vpw,  yBLRadiusNDC = 2*blRadius/vph,
                brRadiusNDC = 2*brRadius/vpw,  yBRRadiusNDC = 2*brRadius/vph;

            var aspect = world.getAspect();
            var zn = world.getZNear(),  zf = world.getZFar();
            var t = zn * Math.tan(world.getFOV() * Math.PI / 360.0),
                b = -t,
                r = aspect*t,
                l = -r;

            // calculate the object coordinates from their NDC coordinates
            var z = -world.getViewDistance();

            // get the position of the origin
            var x = -z*(r-l)/(2.0*zn)*xNDC,
                y = -z*(t-b)/(2.0*zn)*yNDC;

            // get the x and y fill
            var xFill = -z*(r-l)/(2.0*zn)*xFillNDC,
                yFill = -z*(t-b)/(2.0*zn)*yFillNDC;

            // keep some variables giving the overall dimensions of the
            // rectangle. These values are used to calculate consistent
            // texture map coordinates across all pieces.
            this._rectWidth = xFill;  this._rectHeight = yFill;

            // get the stroke size
            var strokeSize = -z*(r-l)/(2.0*zn)*strokeSizeNDC;

            // get the absolute corner radii
            tlRadius = -z*(r-l)/(2.0*zn)*tlRadiusNDC,
            trRadius = -z*(r-l)/(2.0*zn)*trRadiusNDC,
            blRadius = -z*(r-l)/(2.0*zn)*blRadiusNDC,
            brRadius = -z*(r-l)/(2.0*zn)*brRadiusNDC;

            // stroke
            var strokeMaterial = this.makeStrokeMaterial();
            var strokePrim = this.createStroke([x,y],  2*xFill,  2*yFill,  strokeSize,  tlRadius, blRadius, brRadius, trRadius, strokeMaterial);
		strokeMaterial.fitToPrimitive( strokePrim );
            this._primArray.push( strokePrim );
            this._materialNodeArray.push( strokeMaterial.getMaterialNode() );

            // fill
            tlRadius -= strokeSize;  if (tlRadius < 0)  tlRadius = 0.0;
            blRadius -= strokeSize;  if (blRadius < 0)  blRadius = 0.0;
            brRadius -= strokeSize;  if (brRadius < 0)  brRadius = 0.0;
            trRadius -= strokeSize;  if (trRadius < 0)  trRadius = 0.0;
            xFill -= strokeSize;
            yFill -= strokeSize;
            var fillMaterial = this.makeFillMaterial();
            //console.log( "fillMaterial: " + fillMaterial.getName() );
            var fillPrim = this.createFill([x,y],  2*xFill,  2*yFill,  tlRadius, blRadius, brRadius, trRadius, fillMaterial);
		fillMaterial.fitToPrimitive( fillPrim );
            this._primArray.push( fillPrim );
            this._materialNodeArray.push( fillMaterial.getMaterialNode() );

            world.updateObject(this);
        }
    },

    renderQuadraticBezier: {
        value: function(bPts, ctx) {
            if (!bPts)  return;

            var nSegs = (bPts.length - 1)/2.0;
            if (nSegs <= 0)  return;

            var index = 1;
            for (var i=0;  i<nSegs;  i++) {
                ctx.quadraticCurveTo(  bPts[index][0],  bPts[index][1],    bPts[index+1][0], bPts[index+1][1] );
                index += 2;
            }
	    }
    },

    renderPath: {
        value: function(inset, ctx) {
            // various declarations
            var pt,  rad,  ctr,  startPt, bPts;
            var width  = Math.round(this.getWidth()),
                height = Math.round(this.getHeight()),
                hw = 0.5*width,
                hh = 0.5*height;

            pt = [inset, inset];	// top left corner

            var tlRad = this._tlRadius; //top-left radius
            var trRad = this._trRadius;
            var blRad = this._blRadius;
            var brRad = this._brRadius;
            // limit the radii to half the rectangle dimension
            var minDimen = hw < hh ? hw : hh;
            if (tlRad > minDimen)  tlRad = minDimen;
            if (blRad > minDimen)  blRad = minDimen;
            if (brRad > minDimen)  brRad = minDimen;
            if (trRad > minDimen)  trRad = minDimen;

		var viewUtils = require("js/helper-classes/3D/view-utils").ViewUtils;
		var world = this.getWorld();
		viewUtils.pushViewportObj( world.getCanvas() );
		var cop = viewUtils.getCenterOfProjection();
		viewUtils.popViewportObj();
		var xCtr = cop[0] + this._xOffset,					yCtr = cop[1] - this._yOffset;
		var xLeft = xCtr - 0.5*this.getWidth(),				yTop = yCtr - 0.5*this.getHeight();
		var xDist = cop[0] - xLeft,							yDist = cop[1] - yTop;
		var xOff = 0.5*world.getViewportWidth() - xDist,	yOff  = 0.5*world.getViewportHeight() - yDist;

            if ((tlRad <= 0) && (blRad <= 0) && (brRad <= 0) && (trRad <= 0)) {
			ctx.rect(pt[0]+xOff, pt[1]+yOff, width - 2*inset, height - 2*inset);
            } else {
                // get the top left point
                rad = tlRad - inset;
                if (rad < 0)  rad = 0;
                pt[1] += rad;
                if (MathUtils.fpSign(rad) == 0)  pt[1] = inset;
			ctx.moveTo( pt[0]+xOff,  pt[1]+yOff );

                // get the bottom left point
                pt = [inset, height - inset];
                rad = blRad - inset;
                if (rad < 0)  rad = 0;
                pt[1] -= rad;
			ctx.lineTo( pt[0]+xOff,  pt[1]+yOff );

                // get the bottom left curve
                if (MathUtils.fpSign(rad) > 0) {
				ctx.quadraticCurveTo( inset+xOff, height-inset+yOff,  inset+rad+xOff, height-inset+yOff );
                }

                // do the bottom of the rectangle
                pt = [width - inset,  height - inset];
                rad = brRad - inset;
                if (rad < 0)  rad = 0;
                pt[0] -= rad;
			ctx.lineTo( pt[0]+xOff, pt[1]+yOff );

                // get the bottom right arc
                if (MathUtils.fpSign(rad) > 0) {
				ctx.quadraticCurveTo( width-inset+xOff, height-inset+yOff,  width-inset+xOff, height-inset-rad+yOff );
                }

                // get the right of the rectangle
                pt = [width - inset,  inset];
                rad = trRad - inset;
                if (rad < 0)  rad = 0;
                pt[1] += rad;
			ctx.lineTo( pt[0]+xOff, pt[1]+yOff );

                // do the top right corner
                if (MathUtils.fpSign(rad) > 0) {
				ctx.quadraticCurveTo( width-inset+xOff, inset+yOff,  width-inset-rad+xOff, inset+yOff );
                }

                // do the top of the rectangle
                pt = [inset, inset];
                rad = tlRad - inset;
                if (rad < 0)  rad = 0;
                pt[0] += rad;
			ctx.lineTo( pt[0]+xOff, pt[1]+yOff );

                // do the top left corner
                if (MathUtils.fpSign(rad) > 0) {
				ctx.quadraticCurveTo( inset+xOff, inset+yOff, inset+xOff, inset+rad+yOff );
                } else {
				ctx.lineTo( inset+xOff, 2*inset+yOff );
                }
            }
        }
    },

	render: {
		value: function() {
			// get the world
			var world = this.getWorld();
			if (!world)  throw( "null world in rectangle render" );

			 // get the context
			var ctx = world.get2DContext();
			if (!ctx)  return;

			// get some dimensions
			var lw = this._strokeWidth;
			var	w = world.getViewportWidth(),
				h = world.getViewportHeight();

			var c,
				inset,
				gradient,
				colors,
				len,
				n,
				position,
				cs;
			// render the fill
			ctx.beginPath();
			if (this._fillColor) {
				inset = Math.ceil( lw ) - 0.5;

				if(this._fillColor.gradientMode) {
					if(this._fillColor.gradientMode === "radial") {
						var ww = w - 2*lw,  hh = h - 2*lw;
						gradient = ctx.createRadialGradient(w/2, h/2, 0, w/2, h/2, Math.max(ww, hh)/2);
					} else {
						gradient = ctx.createLinearGradient(inset, h/2, w-inset, h/2);
					}
					colors = this._fillColor.color;

					len = colors.length;

					for(n=0; n<len; n++) {
						position = colors[n].position/100;
						cs = colors[n].value;
						gradient.addColorStop(position, "rgba(" + cs.r + "," + cs.g + "," + cs.b + "," + cs.a + ")");
					}

					ctx.fillStyle = gradient;

				} else {
					c = "rgba(" + 255*this._fillColor[0] + "," + 255*this._fillColor[1] + "," + 255*this._fillColor[2] + "," + this._fillColor[3] + ")";
					ctx.fillStyle = c;
				}

				ctx.lineWidth	= lw;
				this.renderPath( inset, ctx );
				ctx.fill();
				ctx.closePath();
			}

			// render the stroke
			ctx.beginPath();
			if (this._strokeColor) {
				inset = Math.ceil( 0.5*lw ) - 0.5;

				if(this._strokeColor.gradientMode) {
					if(this._strokeColor.gradientMode === "radial")
						gradient = ctx.createRadialGradient(w/2, h/2, 0,  w/2, h/2, Math.max(h, w)/2);
					else
						gradient = ctx.createLinearGradient(0, h/2, w, h/2);
					colors = this._strokeColor.color;

					len = colors.length;

					for(n=0; n<len; n++) {
						position = colors[n].position/100;
						cs = colors[n].value;
						gradient.addColorStop(position, "rgba(" + cs.r + "," + cs.g + "," + cs.b + "," + cs.a + ")");
					}

					ctx.strokeStyle = gradient;

				} else {
					c = "rgba(" + 255*this._strokeColor[0] + "," + 255*this._strokeColor[1] + "," + 255*this._strokeColor[2] + "," + this._strokeColor[3] + ")";
					ctx.strokeStyle = c;
				}

				ctx.lineWidth	= lw;
				this.renderPath( inset, ctx );
				ctx.stroke();
				ctx.closePath();
			}
		}
	},

    createStroke: {
        value: function(ctr,  width,  height,  strokeWidth,  tlRad, blRad, brRad, trRad, material) {
            // create the geometry
            return RectangleStroke.create( ctr,  width, height, strokeWidth,  tlRad, blRad,  brRad, trRad, material);
	    }
    },

    createFill: {
        value: function(ctr,  width,  height,  tlRad, blRad, brRad, trRad, material) {
            // create the geometry
            // special the (common) case of no rounded corners
            var prim;

            if ((tlRad <= 0) && (blRad <= 0) && (brRad <= 0) && (trRad <= 0)) {
                prim = RectangleGeometry.create( ctr, width, height, material );
            } else {
                prim = RectangleFill.create( ctr,  width, height,  tlRad, blRad,  brRad, trRad, material);
            }

            return prim;
        }
    },

    collidesWithPoint: {
        value: function(x, y) {
            if(x < this._xOffset) return false;
            if(x > (this._xOffset + this._width)) return false;
            if(y < this._yOffset) return false;
            if(y > (this._yOffset + this._height)) return false;

            return true;
	    }
    },

    containsPoint: {
        value: function(pt, dir) {
            var world = this.getWorld();
            if (!world)  throw( "null world in containsPoint" );

            // get a point on the plane of the circle
            // the point is in NDC, as is the input parameters
            var mat = this.getMatrix();
            var plane = [0,0,1,0];
            plane = MathUtils.transformPlane( plane, mat );
            var projPt = MathUtils.vecIntersectPlane ( pt, dir, plane );

            // transform the projected point back to the XY plane
            //var invMat = mat.inverse();
            var invMat = glmat4.inverse( mat, [] );
            var planePt = MathUtils.transformPoint( projPt, invMat );

            // get the normalized device coordinates (NDC) for
            // the position and radii.
            var	vpw = world.getViewportWidth(),  vph = world.getViewportHeight();
            var	xNDC = 2*this._xOffset/vpw,  yNDC = 2*this._yOffset/vph,
                xRadNDC = this._width/vpw,  yRadNDC = this._height/vph;
            var projMat = world.makePerspectiveMatrix();
            var z = -world.getViewDistance();
            var planePtNDC = planePt.slice(0);
            planePtNDC[2] = z;
            planePtNDC = MathUtils.transformHomogeneousPoint( planePtNDC, projMat );
            planePtNDC = MathUtils.applyHomogeneousCoordinate( planePtNDC );

            // get the center and dimensions of the rect in NDC
            var vpw = world.getViewportWidth(),  vph = world.getViewportHeight();
            var xNDC = 2*this._xOffset/vpw,  yNDC = 2*this._yOffset/vph,
                hw = this._width/vpw,  hh = this._height/vph;

            var x = planePtNDC[0],  y = planePtNDC[1];
            if (x < (xNDC - hw))  return false;
            if (x > (xNDC + hw))  return false;
            if (y < (yNDC - hh))  return false;
            if (y > (yNDC + hh))  return false;

            return true;
        }
    },

    getNearVertex: {
        value: function(pt, dir) {
            var world = this.getWorld();
            if (!world)  throw( "null world in getNearPoint" );

            // get a point on the plane of the circle
            // the point is in NDC, as is the input parameters
            var mat = this.getMatrix();
            var plane = [0,0,1,0];
            plane = MathUtils.transformPlane( plane, mat );
            var projPt = MathUtils.vecIntersectPlane ( pt, dir, plane );

            // transform the projected point back to the XY plane
            //var invMat = mat.inverse();
            var invMat = glmat4.inverse(mat, []);
            var planePt = MathUtils.transformPoint( projPt, invMat );

            // get the normalized device coordinates (NDC) for
            // the position and radii.
            var	vpw = world.getViewportWidth(),  vph = world.getViewportHeight();
            var	xNDC = 2*this._xOffset/vpw,  yNDC = 2*this._yOffset/vph,
                hwNDC = this._width/vpw,  hhNDC = this._height/vph;
            var projMat = world.makePerspectiveMatrix();
            var z = -world.getViewDistance();
            var planePtNDC = planePt.slice(0);
            planePtNDC[2] = z;
            planePtNDC = MathUtils.transformHomogeneousPoint( planePtNDC, projMat );
            planePtNDC = MathUtils.applyHomogeneousCoordinate( planePtNDC );

            // get the near point in NDC
            var x = planePtNDC[0],  y = planePtNDC[1];
            var xMin = xNDC - hwNDC,  xMax = xNDC + hwNDC,
                yMin = yNDC - hhNDC,  yMax = yNDC + hhNDC;

            // compare the point against the 4 corners
                var pt, dist;
                pt = [xMin, yMin, 0];
                dist = VecUtils.vecDist(2, pt, planePtNDC);
                var minPt = pt,  minDist = dist;

                pt = [xMin, yMax, 0];
                dist = VecUtils.vecDist(2, pt, planePtNDC);
                if (dist < minDist) {
                    minDist = dist;
                    minPt = pt;
                }

                pt = [xMax, yMax, 0];
                dist = VecUtils.vecDist(2, pt, planePtNDC);
                if (dist < minDist) {
                    minDist = dist;
                    minPt = pt;
                }

                pt = [xMax, yMin, 0];
                dist = VecUtils.vecDist(2, pt, planePtNDC);
                if (dist < minDist) {
                    minDist = dist;
                    minPt = pt;
                }

            // convert to GL coordinates
            x = minPt[0];  y = minPt[1];
            var aspect = world.getAspect();
            var zn = world.getZNear(),  zf = world.getZFar();
            var	t = zn * Math.tan(world.getFOV() * Math.PI / 360.0),
                b = -t,
                r = aspect*t,
                l = -r;
            var objPt = [0,0,0];
            objPt[0] = -z*(r-l)/(2.0*zn)*x;
            objPt[1] = -z*(t-b)/(2.0*zn)*y;

            // re-apply the transform
            objPt = MathUtils.transformPoint( objPt, mat );

            return objPt;
        }
    },

    getNearPoint: {
        value: function(pt, dir) {
            var world = this.getWorld();
            if (!world)  throw( "null world in getNearPoint" );

            // get a point on the plane of the circle
            // the point is in NDC, as is the input parameters
            var mat = this.getMatrix();
            var plane = [0,0,1,0];
            plane = MathUtils.transformPlane( plane, mat );
            var projPt = MathUtils.vecIntersectPlane ( pt, dir, plane );

            // transform the projected point back to the XY plane
            var invMat = glmat4.inverse(mat, []);
            var planePt = MathUtils.transformPoint( projPt, invMat );

            // get the normalized device coordinates (NDC) for
            // the position and radii.
            var	vpw = world.getViewportWidth(),  vph = world.getViewportHeight();
            var	xNDC = 2*this._xOffset/vpw,  yNDC = 2*this._yOffset/vph,
                hwNDC = this._width/vpw,  hhNDC = this._height/vph;
            var projMat = world.makePerspectiveMatrix();
            var z = -world.getViewDistance();
            var planePtNDC = planePt.slice(0);
            planePtNDC[2] = z;
            planePtNDC = MathUtils.transformHomogeneousPoint( planePtNDC, projMat );
            planePtNDC = MathUtils.applyHomogeneousCoordinate( planePtNDC );

            // get the near point in NDC
            var x = planePtNDC[0],  y = planePtNDC[1];
            var xMin = xNDC - hwNDC,  xMax = xNDC + hwNDC,
                yMin = yNDC - hhNDC,  yMax = yNDC + hhNDC;

            // compare the point against the near point on the 4 sides
            var pt, dist;
            pt = [xMin, y, 0];
            if      (pt[1] < yMin)  pt[1] = yMin;
            else if (pt[1] > yMax)  pt[1] = yMax;
            dist = VecUtils.vecDist(2, pt, planePtNDC);
            var minPt = pt,  minDist = dist;

            pt = [x, yMax, 0];
            if      (pt[0] < xMin)  pt[0] = xMin;
            else if (pt[0] > xMax)  pt[0] = xMax;
            dist = VecUtils.vecDist(2, pt, planePtNDC);
            if (dist < minDist) {
                minDist = dist;
                minPt = pt;
            }

            pt = [xMax, y, 0];
            if      (pt[1] < yMin)  pt[1] = yMin;
            else if (pt[1] > yMax)  pt[1] = yMax;
            dist = VecUtils.vecDist(2, pt, planePtNDC);
            if (dist < minDist) {
                minDist = dist;
                minPt = pt;
            }

            pt = [x, yMin, 0];
            if      (pt[0] < xMin)  pt[0] = xMin;
            else if (pt[0] > xMax)  pt[0] = xMax;
            dist = VecUtils.vecDist(2, pt, planePtNDC);
            if (dist < minDist) {
                minDist = dist;
                minPt = pt;
            }

            // convert to GL coordinates
            x = minPt[0];  y = minPt[1];
            var aspect = world.getAspect();
            var zn = world.getZNear(),  zf = world.getZFar();
            var	t = zn * Math.tan(world.getFOV() * Math.PI / 360.0),
                b = -t,
                r = aspect*t,
                l = -r;
            var objPt = [0,0,0];
            objPt[0] = -z*(r-l)/(2.0*zn)*x;
            objPt[1] = -z*(t-b)/(2.0*zn)*y;

            // re-apply the transform
            objPt = MathUtils.transformPoint( objPt, mat );

            return objPt;
        }
    },

    recalcTexMapCoords: {
        value: function(vrts, uvs) {
            var n = vrts.length/3;
            var ivrt = 0,  iuv = 0;

            for (var i=0;  i<n;  i++) {
                uvs[iuv] = 0.5*(vrts[ivrt]/this._rectWidth + 1);
                iuv++;  ivrt++;
                uvs[iuv] = 0.5*(vrts[ivrt]/this._rectHeight + 1);
                iuv++;  ivrt += 2;
            }
        }
    }
});

var RectangleFill = {};
RectangleFill.create = function( rectCtr,  width, height, tlRad, blRad,  brRad, trRad,  material) {
	var x = rectCtr[0],  y = rectCtr[1],  z = 0.0;
	var	hw = 0.5*width,  hh = 0.5*height;

	// limit the radii to half the rectangle dimension
	var minDimen = hw < hh ? hw : hh;
	if (tlRad > minDimen)  tlRad = minDimen;
	if (blRad > minDimen)  blRad = minDimen;
	if (brRad > minDimen)  brRad = minDimen;
	if (trRad > minDimen)  trRad = minDimen;

	// define some local variables
	this.vertices	= [];
	this.normals	= [];
	this.uvs		= [];
	this.indices	= [];

	// the center of the rectangle is the first vertex
	RectangleFill.pushVertex( x, y, z );

	// traverse the perimiter of the rectangle

	// push the starting point
	RectangleFill.pushVertex( x-hw, y+hh-tlRad,  z);

	// do the left side
	var ctr;
	if (blRad <= 0){
		RectangleFill.pushVertex( x-hw, y-hh, z);
    } else {
		ctr = [x - hw + blRad,  y - hh + blRad, z];
		RectangleFill.getRoundedCorner( ctr,  [x-hw, y-hh+blRad, z],  this.vertices );
	}

	// do the bottom
	if (brRad <= 0) {
		RectangleFill.pushVertex( x+hw, y-hh, z);
    } else {
		ctr = [x + hw - brRad,  y - hh + brRad, z];
		RectangleFill.getRoundedCorner( ctr,  [x+hw-brRad, y-hh, z],  this.vertices );
	}

	// do the right
	if (trRad <= 0) {
		RectangleFill.pushVertex( x+hw, y+hh, z);
    } else {
		ctr = [x + hw - trRad,  y + hh - trRad, z];
		RectangleFill.getRoundedCorner( ctr,  [x+hw, y+hh-trRad, z],  this.vertices );
	}

	// do the top
	if (tlRad <= 0) {
		RectangleFill.pushVertex( x-hw, y+hh, z);
    } else {
		ctr = [x - hw + tlRad,  y + hh - tlRad, z];
		RectangleFill.getRoundedCorner( ctr,  [x-hw+tlRad, y+hh, z],  this.vertices );
	}

	// get the normals and uvs
	var vrt, uv;
	var xMin = x - hw,
		yMin = y - hh;
	var n = [0, 0, 1];
	var nVertices = this.vertices.length / 3;
	for (var i=0;  i<nVertices;  i++) {
		vrt = RectangleFill.getVertex(i);
		RectangleFill.pushNormal( n );
		uv  = RectangleFill.getUV(vrt[0], vrt[1], xMin, width, yMin, height);
		RectangleFill.pushUV( uv );
	}

	// build the triangles
	var nTriangles = nVertices - 2;
	var i = 1,  j = 2;
	for (var iTri=0;  iTri<nTriangles;  iTri++) {
		RectangleFill.pushIndices( 0, j, i );
		i++;
		j++;
	}

	//refine the mesh for vertex deformations
//	if (material) {
//		if (material.hasVertexDeformation()) {
//			var paramRange = material.getVertexDeformationRange();
//			var tolerance = material.getVertexDeformationTolerance();
//			nVertices = ShapePrimitive.refineMesh( this.vertices, this.normals, this.uvs, this.indices, nVertices,  paramRange,  tolerance );
//		}
//	}

	// create the RDGE primitive
	return ShapePrimitive.create(this.vertices, this.normals, this.uvs, this.indices, RDGE.globals.engine.getContext().renderer.TRIANGLES, nVertices);
};

RectangleFill.pushVertex = function( x, y, z ) {
	this.vertices.push( x );
	this.vertices.push( y );
	this.vertices.push( z );
};

RectangleFill.pushNormal = function( n ) {
	this.normals.push( n[0] );
	this.normals.push( n[1] );
	this.normals.push( n[2] );
};

RectangleFill.pushUV = function( uv ) {
	this.uvs.push( uv[0] );
	this.uvs.push( uv[1] );
};

RectangleFill.pushIndices = function( i, j, k ) {
	this.indices.push( i );
	this.indices.push( j );
	this.indices.push( k );
};

RectangleFill.getVertex = function( index ) {
	var i = 3*index;
	return [ this.vertices[i],  this.vertices[i+1],  this.vertices[i+2] ];
};

RectangleFill.getUV = function( x, y, xMin, w, yMin, h) {
	var u = (x - xMin)/w,
		v = (y - yMin)/h;

	var uv = [ u, v ];
	return uv;
};

RectangleFill.getRoundedCorner = function(ctr, startPt,  vertices) {
	var pt0 = startPt.slice();

	// create a matrix to rotate about the center
	var nSegs = 16;
	var angle = 0.5*Math.PI/nSegs;
	var ctrNeg = ctr.slice();
	VecUtils.vecNegate(3, ctrNeg);
	var tNeg  = Matrix.Translation( ctrNeg ),
		rot   = Matrix.RotationZ( angle ),
		trans = Matrix.Translation( ctr );
	var mat = glmat4.multiply( rot, tNeg, [] );
	glmat4.multiply(trans, mat, mat );

	RectangleFill.pushVertex(pt0[0], pt0[1], 0.0 );
	for (var i=0;  i<nSegs;  i++) {
		pt0 = MathUtils.transformPoint( pt0, mat );
		RectangleFill.pushVertex(pt0[0], pt0[1], 0.0 );
	}
};


var RectangleStroke = {};
RectangleStroke.create = function( rectCtr,  width, height, strokeWidth,  tlRad, blRad,  brRad, trRad, material) {
	var x = rectCtr[0],  y = rectCtr[1],  z = 0.0;
	var	hw = 0.5*width,  hh = 0.5*height,  sw = strokeWidth;

	// limit the radii to half the rectangle dimension
	var minDimen = hw < hh ? hw : hh;
	if (tlRad > minDimen)  tlRad = minDimen;
	if (blRad > minDimen)  blRad = minDimen;
	if (brRad > minDimen)  brRad = minDimen;
	if (trRad > minDimen)  trRad = minDimen;

	// define some local variables
	this.vertices	= [];
	this.normals	= [];
	this.uvs		= [];
	this.indices	= [];

	// get the starting points
	if (tlRad == 0) {
		RectangleStroke.pushVertex( x-hw+sw, y+hh-sw, z);
		RectangleStroke.pushVertex( x-hw,    y+hh,    z);
	} else {
		if (tlRad > sw) {
			RectangleStroke.pushVertex( x-hw+sw, y+hh-tlRad, z);
			RectangleStroke.pushVertex( x-hw,    y+hh-tlRad, z);
		} else {
			RectangleStroke.pushVertex( x-hw+tlRad, y+hh-tlRad, z);
			RectangleStroke.pushVertex( x-hw,       y+hh-tlRad, z);
			RectangleStroke.pushVertex( x-hw+sw,    y+hh-sw,    z);
			RectangleStroke.pushVertex( x-hw,       y+hh-sw,    z);
		}
	}

	// get the left side
	if (blRad == 0) {
		RectangleStroke.pushVertex( x-hw+sw, y-hh+sw, z);
		RectangleStroke.pushVertex( x-hw,    y-hh,    z);
	} else {
		if (blRad >= sw) {
			RectangleStroke.pushVertex( x-hw+sw, y-hh+blRad, z);
			RectangleStroke.pushVertex( x-hw,    y-hh+blRad, z);
			var ctr		 =	[x-hw+blRad,  y-hh+blRad, z],
				insidePt =	[x-hw+sw,     y-hh+blRad, z],
				outsidePt = [x-hw,        y-hh+blRad, z];
			RectangleStroke.getRoundedCorner( ctr, insidePt,  outsidePt,  this.vertices );
		} else {
			RectangleStroke.pushVertex( x-hw+sw,  y-hh+sw,    z);
			RectangleStroke.pushVertex( x-hw,     y-hh+blRad, z);
			var ctr		 =	[x-hw+blRad,  y-hh+blRad, z],
				insidePt =	[x-hw+blRad,  y-hh+blRad, z],
				outsidePt = [x-hw,        y-hh+blRad, z];
			RectangleStroke.getRoundedCorner( ctr, insidePt, outsidePt, this.vertices  );

			RectangleStroke.pushVertex( x-hw+sw,  y-hh+sw, z);
			RectangleStroke.pushVertex( x-hw+sw,  y-hh,    z);
		}
	}

	// get the bottom
	if (brRad == 0) {
		RectangleStroke.pushVertex( x+hw-sw, y-hh+sw, z);
		RectangleStroke.pushVertex( x+hw,    y-hh,    z);
	} else {
		RectangleStroke.pushVertex( x+hw-brRad,    y-hh+sw, z);
		RectangleStroke.pushVertex( x+hw-brRad,    y-hh,    z);
		if (brRad >= sw) {
			var ctr		 =	[x+hw-brRad,  y-hh+brRad, z],
				insidePt =	[x+hw-brRad,  y-hh+sw,    z],
				outsidePt = [x+hw-brRad,  y-hh,       z];
			RectangleStroke.getRoundedCorner( ctr, insidePt,  outsidePt,  this.vertices );
		} else {
			RectangleStroke.pushVertex( x+hw-sw,    y-hh+sw, z);
			RectangleStroke.pushVertex( x+hw-brRad,    y-hh, z);
			var ctr		 =	[x+hw-brRad,  y-hh+brRad, z],
				insidePt =	[x+hw-brRad,  y-hh+brRad, z],
				outsidePt = [x+hw-brRad,  y-hh,       z];
			RectangleStroke.getRoundedCorner( ctr, insidePt, outsidePt,  this.vertices );
			RectangleStroke.pushVertex( x+hw-sw,    y-hh+sw, z);
			RectangleStroke.pushVertex( x+hw,       y-hh+sw, z);
		}
	}

	// get the right
	if (trRad == 0) {
		RectangleStroke.pushVertex( x+hw-sw, y+hh-sw, z);
		RectangleStroke.pushVertex(    x+hw,    y+hh, z);
	} else {
		if (trRad >= sw) {
			RectangleStroke.pushVertex( x+hw-sw,  y+hh-trRad, z);
			RectangleStroke.pushVertex( x+hw,     y+hh-trRad, z);
			var ctr		 =	[x+hw-trRad,  y+hh-trRad, z],
				insidePt =	[x+hw-sw,     y+hh-trRad, z],
				outsidePt = [x+hw,        y+hh-trRad, z];
			RectangleStroke.getRoundedCorner( ctr, insidePt,  outsidePt,  this.vertices );
		} else {
			RectangleStroke.pushVertex( x+hw-sw,  y+hh-sw,    z);
			RectangleStroke.pushVertex( x+hw,     y+hh-trRad, z);
			var ctr		 =	[x+hw-trRad,  y+hh-trRad, z],
				insidePt =	[x+hw-trRad,  y+hh-trRad, z],
				outsidePt = [x+hw,        y+hh-trRad, z];
			RectangleStroke.getRoundedCorner( ctr, insidePt, outsidePt,  this.vertices );
			RectangleStroke.pushVertex( x+hw-sw,  y+hh-sw, z);
			RectangleStroke.pushVertex( x+hw-sw,  y+hh,    z);
		}
	}

	// get the top
	if (tlRad == 0) {
		RectangleStroke.pushVertex( x-hw+sw,  y+hh-sw, z);
		RectangleStroke.pushVertex( x-hw,     y+hh,    z);
	} else {
		if (tlRad >= sw) {
			RectangleStroke.pushVertex( x-hw+tlRad,  y+hh-sw, z);
			RectangleStroke.pushVertex( x-hw+tlRad,  y+hh,    z);
			var ctr		 =	[x-hw+tlRad,  y+hh-tlRad, z],
				insidePt =	[x-hw+tlRad,  y+hh-sw, z],
				outsidePt = [x-hw+tlRad,  y+hh, z];
			RectangleStroke.getRoundedCorner( ctr, insidePt,  outsidePt,  this.vertices );
		} else {
			RectangleStroke.pushVertex( x-hw+sw,     y+hh-sw, z);
			RectangleStroke.pushVertex( x-hw+tlRad,  y+hh,    z);
			var ctr		 =	[x-hw+tlRad,  y+hh-tlRad, z],
				insidePt =	[x-hw+tlRad,  y+hh-tlRad, z],
				outsidePt = [x-hw+tlRad,  y+hh, z];
			RectangleStroke.getRoundedCorner( ctr, insidePt, outsidePt,  this.vertices );
		}
	}

	// get the normals and uvs
	var vrt, uv;
	var xMin = x - hw,
		yMin = y - hh;
	var n = [0, 0, 1];
	var nVertices = this.vertices.length / 3;
	for (var i=0;  i<nVertices;  i++) {
		vrt = RectangleStroke.getVertex(i);
		RectangleStroke.pushNormal( n );
		uv  = RectangleStroke.getUV(vrt[0], vrt[1], xMin, width, yMin, height);
		RectangleStroke.pushUV( uv );
	}

	// build the triangles
	var nTriangles = nVertices - 2;
	var i = 0,  j = 1, k = 2;
	var reverse = false;
	for (var iTri=0;  iTri<nTriangles;  iTri++) {
		// we created a triangle strip, so each sequential triangle has the opposite orientation than its predecessor
		if (!reverse) {
			RectangleStroke.pushIndices( k, j, i );
        } else {
			RectangleStroke.pushIndices( i, j, k );
        }

		reverse = !reverse;

		i++;
		j++;
		k++;
	}

	//refine the mesh for vertex deformations
//	if (material) {
//		if (material.hasVertexDeformation()) {
//			var paramRange = material.getVertexDeformationRange();
//			var tolerance = material.getVertexDeformationTolerance();
//			nVertices = ShapePrimitive.refineMesh( this.vertices, this.normals, this.uvs, this.indices, nVertices,  paramRange,  tolerance );
//		}
//	}

	// create the RDGE primitive
	return ShapePrimitive.create(this.vertices, this.normals, this.uvs, this.indices, RDGE.globals.engine.getContext().renderer.TRIANGLES, nVertices);
};

RectangleStroke.getRoundedCorner = function( ctr, insidePt, outsidePt ) {
	var pt0 = insidePt.slice(),
		pt1 = outsidePt.slice();

	// create a matrix to rotate about the center
	var nSegs = 16;
	var angle = 0.5*Math.PI/nSegs;
	var ctrNeg = ctr.slice();
	VecUtils.vecNegate(3, ctrNeg);
	var tNeg  = Matrix.Translation( ctrNeg ),
		rot   = Matrix.RotationZ( angle ),
		trans = Matrix.Translation( ctr );
	var mat = glmat4.multiply( rot, tNeg, [] );
	glmat4.multiply(trans, mat, mat );

	RectangleStroke.pushVertex(pt0[0], pt0[1], 0.0 );
	RectangleStroke.pushVertex(pt1[0], pt1[1], 0.0 );
	for (var i=0;  i<nSegs;  i++) {
		pt0 = MathUtils.transformPoint( pt0, mat );
		pt1 = MathUtils.transformPoint( pt1, mat );

		RectangleStroke.pushVertex(pt0[0], pt0[1], 0.0 );
		RectangleStroke.pushVertex(pt1[0], pt1[1], 0.0 );
	}
};

RectangleStroke.pushVertex	= RectangleFill.pushVertex;
RectangleStroke.pushNormal	= RectangleFill.pushNormal;
RectangleStroke.pushUV		= RectangleFill.pushUV;
RectangleStroke.pushIndices	= RectangleFill.pushIndices;
RectangleStroke.getVertex	= RectangleFill.getVertex;
RectangleStroke.getUV		= RectangleFill.getUV;

var RectangleGeometry = {};
RectangleGeometry.create = function( ctr,  width, height, material ) {
	var x = ctr[0],  y = ctr[1],  z = 0.0;
	var	hw = 0.5*width,  hh = 0.5*height;

	// define some local variables
	this.vertices	= [];
	this.normals	= [];
	this.uvs		= [];
	this.indices	= [];

	// create the 4 vertices
	var nVertices = 4;
	RectangleGeometry.pushVertex( x-hw, y+hh, z);
	RectangleGeometry.pushVertex( x-hw, y-hh, z);
	RectangleGeometry.pushVertex( x+hw, y-hh, z);
	RectangleGeometry.pushVertex( x+hw, y+hh, z);

	// create the uv values for each vertex
	RectangleGeometry.pushUV( [0, 0] );
	RectangleGeometry.pushUV( [0, 1] );
	RectangleGeometry.pushUV( [1, 1] );
	RectangleGeometry.pushUV( [1, 0] );


	// create the per-vertex normals
	var n = [0, 0, 1];
	RectangleGeometry.pushNormal( n );
	RectangleGeometry.pushNormal( n );
	RectangleGeometry.pushNormal( n );
	RectangleGeometry.pushNormal( n );

	// create the 2 triangles
//	RectangleGeometry.pushIndices( 0, 1, 2 );
//	RectangleGeometry.pushIndices( 2, 3, 0 );
	RectangleGeometry.pushIndices( 2, 1, 0 );
	RectangleGeometry.pushIndices( 0, 3, 2 );

	//refine the mesh for vertex deformations
//	if (material)
//	{
//		if (material.hasVertexDeformation())
//		{
//			var paramRange = material.getVertexDeformationRange();
//			var tolerance = material.getVertexDeformationTolerance();
//			nVertices = ShapePrimitive.refineMesh( this.vertices, this.normals, this.uvs, this.indices, nVertices,  paramRange,  tolerance );
//		}
//	}

	// create the RDGE primitive
	return ShapePrimitive.create(this.vertices, this.normals, this.uvs, this.indices, RDGE.globals.engine.getContext().renderer.TRIANGLES, nVertices);
};

RectangleGeometry.pushVertex	= RectangleFill.pushVertex;
RectangleGeometry.pushNormal	= RectangleFill.pushNormal;
RectangleGeometry.pushUV		= RectangleFill.pushUV;
RectangleGeometry.pushIndices	= RectangleFill.pushIndices;
RectangleGeometry.getVertex		= RectangleFill.getVertex;
RectangleGeometry.getUV			= RectangleFill.getUV;