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

///////////////////////////////////////////////////////////////////////
// Class RuntimeGeomObj
//      Super class for all geometry classes
///////////////////////////////////////////////////////////////////////
function RuntimeGeomObj()
{
    ///////////////////////////////////////////////////////////////////////
    // Constants
    ///////////////////////////////////////////////////////////////////////
	this.GEOM_TYPE_RECTANGLE		=  1;
	this.GEOM_TYPE_CIRCLE			=  2;
	this.GEOM_TYPE_LINE             =  3;
	this.GEOM_TYPE_PATH			    =  4;
	this.GEOM_TYPE_CUBIC_BEZIER     =  5;
	this.GEOM_TYPE_UNDEFINED		= -1;
	
    ///////////////////////////////////////////////////////////////////////
    // Instance variables
    ///////////////////////////////////////////////////////////////////////
    this._children;

    // stroke and fill colors
    this._strokeColor	= [0,0,0,0];
    this._fillColor		= [0,0,0,0];

	// array of materials
	this._materials = [];

    ///////////////////////////////////////////////////////////////////////
    // Property accessors
    ///////////////////////////////////////////////////////////////////////

	this.geomType	= function()		{  return this.GEOM_TYPE_UNDEFINED;	}

	this.setWorld	= function(w)		{  this._world = w;					}
	this.getWorld	= function()		{  return this._world;				}

    ///////////////////////////////////////////////////////////////////////
    // Methods
    ///////////////////////////////////////////////////////////////////////
    this.makeStrokeMaterial = function()
    {
    }

    this.makeFillMaterial = function()
    {
    }


    this.render = function()
    {
    }
    
	this.addChild = function( child )
	{
		if (!this._children)  this._children = [];
		this._children.push( child );
	}

    this.import = function()
    {
	}

	this.importMaterials = function(importStr)
	{
		var nMaterials = Number( getPropertyFromString( "nMaterials: ", importStr )  );
		for (var i=0;  i<nMaterials;  i++)
		{
			var matNodeName = getPropertyFromString( "materialNodeName: ",	importStr );

			var mat;
			var materialType = getPropertyFromString( "material: ",	importStr );
			switch (materialType)
			{
				case "flat":			mat = new RuntimeFlatMaterial();				break;
				case "radialGradient":  mat = new RuntimeRadialGradientMaterial();		break;
				case "linearGradient":  mat = new RuntimeLinearGradientMaterial();		break;
				case "bumpMetal":		mat = new RuntimeBumpMetalMaterial();			break;
				case "uber":			mat = new RuntimeUberMaterial();				break;

				case "deform":
				case "water":
				case "tunnel":
				case "reliefTunnel":
				case "squareTunnel":
				case "twist":
				case "fly":
				case "julia":
				case "mandel":
				case "star":
				case "zinvert":
				case "keleidoscope":
				case "pulse":			mat = new RuntimePulseMaterial();				break;

				default:
					console.log( "material type: " + materialType + " is not supported" );
					break;
			}

			if (mat)
			{
				mat.import( importStr );
				mat._materialNodeName = matNodeName;
				this._materials.push( mat );
			}

			var endIndex = importStr.indexOf( "endMaterial\n" );
			if (endIndex < 0)  break;
			importStr = importStr.substr( endIndex );
		}
	}

	////////////////////////////////////////////////////////////////////
	// vector function

	this.vecAdd = function( dimen,  a, b )
	{
        var rtnVec;
        if ((a.length < dimen) || (b.length < dimen))
        {
            throw new Error( "dimension error in vecAdd" );
        }

        rtnVec = [];
        for (var i=0;  i<dimen;  i++)
            rtnVec[i] = a[i] + b[i];

        return rtnVec;
    }


	this.vecSubtract =  function( dimen, a, b )
	{
        var rtnVec;
        if ((a.length < dimen) || (b.length < dimen))
        {
            throw new Error( "dimension error in vecSubtract" );
        }

        rtnVec = [];
        for (var i=0;  i<dimen;  i++)
            rtnVec[i] = a[i] - b[i];

        return rtnVec;
    }

    this.vecDot = function( dimen,  v0, v1 )
	{
        if ((v0.length < dimen) || (v1.length < dimen))
        {
            throw new Error( "dimension error in vecDot" );
        }

        var sum = 0.0;
        for (var i=0;  i<dimen;  i++)
            sum += v0[i] * v1[i];

        return sum;
    }

	this.vecMag = function( dimen, vec )
	{
        var sum = 0.0;
        for (var i=0;  i<dimen;  i++)
            sum += vec[i]*vec[i];
        return Math.sqrt( sum );
    }

	this.vecScale = function(dimen, vec, scale)
	{
        for (var i=0;  i<dimen;  i++)
            vec[i] *= scale;

        return vec;
    }

    this.vecNormalize = function(dimen, vec, len)
	{
        var rtnVec;
		if (!len)  len = 1.0;

        var sum = 0.0;
        for (var i=0;  i<dimen;  i++)
            sum += vec[i]*vec[i];
        sum = Math.sqrt( sum );

        if (Math.abs(sum) >= 0.001)
        {
            var scale = len/sum;
            rtnVec = [];
            for (var i=0;  i<dimen;  i++)
                rtnVec[i] = vec[i]*scale;
        }

        return rtnVec;
    },

	this.transformPoint = function( srcPt, mat )
    {
        var pt = srcPt.slice(0);
        var	x = this.vecDot(3,  pt, [mat[0], mat[4], mat[ 8]] ) + mat[12],
            y = this.vecDot(3,  pt, [mat[1], mat[5], mat[ 9]] ) + mat[13],
            z = this.vecDot(3,  pt, [mat[2], mat[6], mat[10]] ) + mat[14];

        return [x, y, z];
    }
}

function getPropertyFromString( prop, str )
{
	var index = str.indexOf( prop );
	if (index < 0)  throw new Error( "property " + prop + " not found in string: " + str);

	var rtnStr = str.substr( index+prop.length );
	index = rtnStr.indexOf( "\n" );
	if (index >= 0)
		rtnStr = rtnStr.substr(0, index);

	return rtnStr;
}

///////////////////////////////////////////////////////////////////////
// Class RuntimeRectangle
///////////////////////////////////////////////////////////////////////
function RuntimeRectangle()
{
	// inherit the members of RuntimeGeomObj
	this.inheritedFrom = RuntimeGeomObj;
	this.inheritedFrom();

	this.import = function( importStr )
	{
		this._xOffset			= Number( getPropertyFromString( "xoff: ",			importStr )  );
		this._yOffset			= Number( getPropertyFromString( "yoff: ",			importStr )  );
		this._width				= Number( getPropertyFromString( "width: ",		importStr )  );
		this._height			= Number( getPropertyFromString( "height: ",		importStr )  );
		this._strokeWidth		= Number( getPropertyFromString( "strokeWidth: ",	importStr )  );
		this._innerRadius		= Number( getPropertyFromString( "innerRadius: ",	importStr )  );
		this._strokeStyle		= Number( getPropertyFromString( "strokeStyle: ",	importStr )  );
		var strokeMaterialName	= getPropertyFromString( "strokeMat: ",	importStr );
		var fillMaterialName	= getPropertyFromString( "fillMat: ",		importStr );
		this._strokeStyle		=  getPropertyFromString( "strokeStyle: ",	importStr );
		this._fillColor			=  eval( "[" + getPropertyFromString( "fillColor: ",	importStr ) + "]" );
		this._strokeColor		=  eval( "[" + getPropertyFromString( "strokeColor: ",	importStr ) + "]" );
		this._tlRadius			=  Number( getPropertyFromString( "tlRadius: ",	importStr )  );
		this._trRadius			=  Number( getPropertyFromString( "trRadius: ",	importStr )  );
		this._blRadius			=  Number( getPropertyFromString( "blRadius: ",	importStr )  );
		this._brRadius			=  Number( getPropertyFromString( "brRadius: ",	importStr )  );

		this.importMaterials( importStr );
	}

	this.renderPath = function( inset, ctx )
	{
		// various declarations
		var pt,  rad,  ctr,  startPt, bPts;
		var width  = Math.round(this._width),
			height = Math.round(this._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;

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

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

			// get the bottom left curve
			if (rad > 0.001)
				ctx.quadraticCurveTo( inset, height-inset,  inset+rad, height-inset );

			// 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], pt[1] );

			// get the bottom right arc
			if (rad > 0.001)
				ctx.quadraticCurveTo( width-inset, height-inset,  width-inset, height-inset-rad );

			// 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], pt[1] );

			// do the top right corner
			if (rad > 0.001)
				ctx.quadraticCurveTo( width-inset, inset,  width-inset-rad, inset );

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

			// do the top left corner
			if (rad > 0.001)
				ctx.quadraticCurveTo( inset, inset, inset, inset+rad );
			else
				ctx.lineTo( inset, 2*inset );
		}
	}

    this.render = 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();
		
		// render the fill
		ctx.beginPath();
		if (this._fillColor)
		{
			var c = "rgba(" + 255*this._fillColor[0] + "," + 255*this._fillColor[1] + "," + 255*this._fillColor[2] + "," + this._fillColor[3] + ")";  
			ctx.fillStyle = c;

			ctx.lineWidth	= lw;
			var inset = Math.ceil( lw ) + 0.5;
			this.renderPath( inset, ctx );
			ctx.fill();
			ctx.closePath();
		}

		// render the stroke
		ctx.beginPath();
		if (this._strokeColor)
		{
			var c = "rgba(" + 255*this._strokeColor[0] + "," + 255*this._strokeColor[1] + "," + 255*this._strokeColor[2] + "," + this._strokeColor[3] + ")";  
			ctx.strokeStyle = c;

			ctx.lineWidth	= lw;
			var inset = Math.ceil( 0.5*lw ) + 0.5;
			this.renderPath( inset, ctx );
			ctx.stroke();
			ctx.closePath();
		}
    }
}

///////////////////////////////////////////////////////////////////////
// Class RuntimeOval
///////////////////////////////////////////////////////////////////////
function RuntimeOval()
{
	// inherit the members of RuntimeGeomObj
	this.inheritedFrom = RuntimeGeomObj;
	this.inheritedFrom();

	this.import = function( importStr )
	{
		this._xOffset			= Number( getPropertyFromString( "xoff: ",			importStr ) );
		this._yOffset			= Number( getPropertyFromString( "yoff: ",			importStr ) );
		this._width				= Number( getPropertyFromString( "width: ",		importStr ) );
		this._height			= Number( getPropertyFromString( "height: ",		importStr ) );
		this._strokeWidth		= Number( getPropertyFromString( "strokeWidth: ",	importStr ) );
		this._innerRadius		= Number( getPropertyFromString( "innerRadius: ",	importStr ) );
		this._strokeStyle		= getPropertyFromString( "strokeStyle: ",	importStr );
		var strokeMaterialName	= getPropertyFromString( "strokeMat: ",	importStr );
		var fillMaterialName	= getPropertyFromString( "fillMat: ",		importStr );
		this._fillColor			=  eval( "[" + getPropertyFromString( "fillColor: ",	importStr ) + "]" );
		this._strokeColor		=  eval( "[" + getPropertyFromString( "strokeColor: ",	importStr ) + "]" );
		
		this.importMaterials( importStr );
	}

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

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

		// declare some variables
		var p0, p1;
		var x0, y1,   x1, y1;

		// create the matrix
		var lineWidth = this._strokeWidth;
		var innerRad  = this._innerRadius;
		var xScale = 0.5*this._width - lineWidth,
			yScale = 0.5*this._height - lineWidth;

		// translate
		var xCtr = 0.5*world.getViewportWidth() + this._xOffset,
			yCtr = 0.5*world.getViewportHeight() + this._yOffset;
		var mat = Matrix.create( [
							[ xScale,     0.0,  0.0,  xCtr],
							[    0.0,  yScale,  0.0,  yCtr],
							[    0.0,     0.0,  1.0,   0.0],
							[    0.0,     0.0,  0.0,   1.0]
						] );

		// get a bezier representation of the circle
		var bezPts = this.circularArcToBezier( [0,0,0],  [1,0,0], 2.0*Math.PI );
		if (bezPts)
		{
			var n = bezPts.length;

			// set up the fill style
			ctx.beginPath();
			ctx.lineWidth = 0;
			if (this._fillColor)
			{
				var c = "rgba(" + 255*this._fillColor[0] + "," + 255*this._fillColor[1] + "," + 255*this._fillColor[2] + "," + this._fillColor[3] + ")";  
				ctx.fillStyle = c;

				// draw the fill
				ctx.beginPath();
				var p = this.transformPoint( bezPts[0],   mat );
				ctx.moveTo( p[0],  p[1] );
				var index = 1;
				while (index < n)
				{
					p0 = this.transformPoint( bezPts[index],  mat );
					p1 = this.transformPoint( bezPts[index+1],  mat );

					x0 = p0[0];  y0 = p0[1];
					x1 = p1[0];  y1 = p1[1];
					ctx.quadraticCurveTo( x0,  y0,  x1, y1 );
					index += 2;
				}

				if ( innerRad > 0.001)
				{
					xScale = 0.5*innerRad*this._width;
					yScale = 0.5*innerRad*this._height;
					mat[0] = xScale;
					mat[5] = yScale;

					// get the bezier points
					var bezPts = this.circularArcToBezier( Vector.create([0,0,0]), Vector.create([1,0,0]), -2.0*Math.PI );
					if (bezPts)
					{
						var n = bezPts.length;
						p = this.transformPoint( bezPts[0],   mat );
						ctx.moveTo( p[0],  p[1] );
						index = 1;
						while (index < n)
						{
							p0 = this.transformPoint( bezPts[index],    mat );
							p1 = this.transformPoint( bezPts[index+1],  mat );

							var x0 = p0[0],  y0 = p0[1],
								x1 = p1[0],  y1 = p1[1];
							ctx.quadraticCurveTo( x0,  y0,  x1, y1 );
							index += 2;
						}
					}
				}

				// fill the path
				ctx.fill();
			}

			// calculate the stroke matrix
			xScale = 0.5*this._width  - 0.5*lineWidth;
			yScale = 0.5*this._height - 0.5*lineWidth;
			mat[0] = xScale;
			mat[5] = yScale;

			// set up the stroke style
			ctx.beginPath();
			ctx.lineWidth	= lineWidth;
			if (this._strokeColor)
			{
				var c = "rgba(" + 255*this._strokeColor[0] + "," + 255*this._strokeColor[1] + "," + 255*this._strokeColor[2] + "," + this._strokeColor[3] + ")";  
				ctx.strokeStyle = c;
			
				// draw the stroke
				p = this.transformPoint( bezPts[0],   mat );
				ctx.moveTo( p[0],  p[1] );
				index = 1;
				while (index < n)
				{
					var p0 = this.transformPoint( bezPts[index],  mat );
					var p1 = this.transformPoint( bezPts[index+1],  mat );

					var x0 = p0[0],  y0 = p0[1],
						x1 = p1[0],  y1 = p1[1];
					ctx.quadraticCurveTo( x0,  y0,  x1, y1 );
					index += 2;
				}

				if (innerRad > 0.01)
				{
					// calculate the stroke matrix
					xScale = 0.5*innerRad*this._width  - 0.5*lineWidth;
					yScale = 0.5*innerRad*this._height - 0.5*lineWidth;
					mat[0] = xScale;
					mat[5] = yScale;
			
					// draw the stroke
					p = this.transformPoint( bezPts[0],   mat );
					ctx.moveTo( p[0],  p[1] );
					index = 1;
					while (index < n)
					{
						var p0 = this.transformPoint( bezPts[index],  mat );
						var p1 = this.transformPoint( bezPts[index+1],  mat );

						var x0 = p0[0],  y0 = p0[1],
							x1 = p1[0],  y1 = p1[1];
						ctx.quadraticCurveTo( x0,  y0,  x1, y1 );
						index += 2;
					}
				}

				// render the stroke
				ctx.stroke();
			}
		}
    }

    ///////////////////////////////////////////////////////////////////////
	// this function returns the quadratic Bezier approximation to the specified
	// circular arc.  The input can be 2D or 3D, determined by the minimum dimension
	// of the center and start point.
	// includedAngle is in radians, can be positiveor negative
	this.circularArcToBezier= function( ctr_, startPt_, includedAngle )
	{
        var dimen = 3;
        var ctr = ctr_.slice();
        var startPt = startPt_.slice();

        // make sure the start point is good
        var pt = this.vecSubtract(dimen, startPt, ctr);
        var rad = this.vecMag(dimen, pt);

        if ((dimen != 3) || (rad <= 0) || (includedAngle === 0))
        {
            if (dimen != 3)  console.log( "circularArcToBezier works for 3 dimensional points only.  Was " + dimen );
            return [ startPt.slice(0), startPt.slice(0), startPt.slice(0) ];
        }

        // determine the number of segments.  45 degree span maximum.
        var nSegs = Math.ceil( Math.abs(includedAngle)/(0.25*Math.PI) );
        if (nSegs <= 0)  return [ startPt.slice(0), startPt.slice(0), startPt.slice(0) ];
        var dAngle = includedAngle/nSegs;

        // determine the length of the center control point from the circle center
        var cs = Math.cos( 0.5*Math.abs(dAngle) ),  sn = Math.sin( 0.5*Math.abs(dAngle) );
        var  c = rad*sn;
        var  h = c*sn/cs;
        var  d = rad*cs + h;

        var rtnPts = [ this.vecAdd(dimen, pt, ctr) ];
        var rotMat = Matrix.RotationZ( dAngle );
        for ( var i=0;  i<nSegs;  i++)
        {
            // get the next end point
            var pt2 = this.transformPoint( pt, rotMat );

            // get the next center control point
            var midPt = this.vecAdd(3, pt, pt2);
            this.vecScale(dimen, midPt, 0.5);
            midPt = this.vecNormalize( dimen, midPt, d );

            // save the next segment
            rtnPts.push( this.vecAdd(dimen, midPt, ctr) );
            rtnPts.push( this.vecAdd(dimen,   pt2, ctr) );

            // advance for the next segment
            pt = pt2;
        }
        return rtnPts;
	}
}