/*
Copyright (c) 2009, Dylan Oudyk. All rights reserved.
Code licensed under the BSD License:

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 OWNER 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.

version: 0.0.1
*/

/**
 * @author dylan oudyk
 */

/**
 * Object used for hacking in
 * transform origin support in IE
 * You could probably also use some of these capabilities
 * really for parsing out positions like the background position property or something
 * @param {Object} str
 */
var OriginParser = {
    
	/**
	 * Gets an origin object for the string
	 * @param {String} str
	 */
	getOrigin: function(str){
    
        var x = 50, y = 50, unitX = '%', unitY = '%', reOrigin = /\-*\w+[px|\%]*/g, matches, matchX, matchY, reLength = /\-*\d+(?=[px|\%])/, reUnit = /(px)|\%/i, reXPosition = /left|center|right/i, reYPosition = /top|center|bottom/i;
        
        function getNumberFromPosition(strPosition){
            if (/left|top/i.test(strPosition)) {
                return 0;
            }
            
            if (/right|bottom/i.test(strPosition)) {
                return 100;
            }
            //default to 50%
            return 50;
        }
        
        if (reOrigin.test(str)) {
        
            matches = str.match(reOrigin);
            matchX = matches[0];
            matchY = matches[1];
            
            //first find our origin x value,
            //we'll test for units first
            if (reLength.test(matchX)) {
                x = parseInt(matchX.match(reLength)[0]);
                unitX = matchX.match(reUnit)[0].toLowerCase();
                
            }
            else 
                if (reXPosition.test(matchX)) {
                    x = getNumberFromPosition(matchX);
                }
            
            if (matchY) {
            
                if (reLength.test(matchY)) {
                    y = parseInt(matchY.match(reLength)[0]);
                    unitY = matchY.match(reUnit)[0].toLowerCase();
                }
                else 
                    if (reYPosition.test(matchY)) {
                        y = getNumberFromPosition(matchY);
                    }
                
            }
        }
        
        return this._createOriginObj(x, y, unitX, unitY);
    },
    
	/**
	 * Gets the x,y values in pixels relative to the top left corner of
	 * an element with the specified origin Style
	 * @param {Object} originStyle
	 * @param {Object} relativeWidth
	 * @param {Object} relativeHeight
	 */
    getOriginInRelativePixels: function(originStyle, relativeWidth, relativeHeight){
    
        var o = this.getOrigin(originStyle);
        
        var x = o.unitX === '%' ? relativeWidth * (o.x / 100) : o.x;
        var y = o.unitY === '%' ? relativeHeight * (o.y / 100) : o.y;
        
        return {
            x: x,
            y: y
        };
    },
    

	/**
	 * @private
	 * Creates an origin object
	 * @param {Number} x
	 * @param {Number} y
	 * @param {String} unitX
	 * @param {String} unitY
	 * On second thought I should have unified
	 * the value with the unit into a single object
	 */
    _createOriginObj: function(x, y, unitX, unitY){
        return {
            x: x,
            y: y,
            unitX: unitX,
            unitY: unitY
        };
    }
};


function get(id){
    return document.getElementById(id);
}

/**
 * Matrix object that i'm only using for
 * Transforms, good ol' YAGNI coding
 */
function TMatrix(mItems){

    //default off to the identity matrix
    var _items = mItems || [[1, 0, 0], [0, 1, 0], [0, 0, 1]];
    
    this.items = function(){
        return _items;
    };
}

(function(){
	
function toRadians(angle){
    return (angle * Math.PI) / 180;
}

function toAngle(radian){
    return (radian * 180) / Math.PI;
}

TMatrix.prototype = {

    /**
     * Multiplies a matrix
     * and returns a new instance with the result
     * @param {Object} matrix
     */
    multiply: function(m){
    
        var i1 = this.items(), r1 = i1[0], r2 = i1[1], r3 = i1[2];
        
        
        if ("number" === typeof m && isFinite(m)) {
        
        
            return new TMatrix([[r1[0] * m, r1[1] * m, r1[2] * m], [r2[0] * m, r2[1] * m, r2[2] * m], [r3[0] * m, r3[1] * m, r3[2] * m]]);
        }
        
        var result = [[0, 0, 0], [0, 0, 0], [0, 0, 0]], row;
        
        
        var i2 = m.items(), p1 = i2[0], p2 = i2[1], p3 = i2[2];
        for (var i = 0; i < 3; i++) {
            for (var j = 0; j < 3; j++) {
                row = i1[i]
                
                for (var k = 0; k < 3; k++) {
                    result[i][j] += row[k] * i2[k][j];
                }                
            }
        }        
        return new TMatrix(result);        
    },
    
    skewX: function(angle){
        var tanAngle = Math.tan(toRadians(angle));
        return this.multiply(new TMatrix([[1, tanAngle, 0], [0, 1, 0], [0, 0, 1]]));
    },
    
    skewY: function(angle){
        var tanAngle = Math.tan(toRadians(angle));
        return this.multiply(new TMatrix([[1, 0, 0], [tanAngle, 1, 0], [0, 0, 1]]));
    },
    
    rotate: function(angle){
		//special casing
		if (0 === angle || 0 === angle % 360){
			return TMatrix.Identity ();
		}
    
        var r = toRadians(angle), cosAngle = Math.cos(r), sinAngle = Math.sin(r);
        return this.multiply(new TMatrix([[cosAngle, -sinAngle, 0], [sinAngle, cosAngle, 0], [0, 0, 1]]));        
    },
    
    scale: function(val){
        return this.multiply(new TMatrix([[val, 0, 0], [0, val, 0], [0, 0, 1]]));
    },
    
    scaleNonUniform: function(x, y){
        return this.multiply(new TMatrix([[x, 0, 0], [0, y, 0], [0, 0, 1]]));
    },
    
    rotateFromVector: function(x, y){
        return this.rotate(toAngle(Math.atan2(y, x)));
    }
};

	TMatrix.Identity = function(){
	    return new TMatrix();
	};

	/**
	 * Gets a matrix described with json notation
	 * each json key represents a TMatrix instance
	 * method.  Use multiply at your own risk.
	 * Heck use all this code at your own risk :D
	 * @param {Object} transforms
	 */
	TMatrix.describedAs = function (transforms){
		var matrix = this.Identity(), args;
                    
        for (var transform in transforms) {
			if (!transforms.hasOwnProperty(transform)) {
            	continue;
            }
                        
            args = transforms[transform];
			//duck typing here, an isArray method would be much preferred
            if (!(args.hasOwnProperty('length') && ('number' === typeof args.length))) {
            	args = [args];
            }            
                        
        	matrix = matrix[transform].apply(matrix, args);
        }
                    
	    return matrix;
	}; 
}())


var theTransformer = (function(){
	
	/**
 * For Mozilla and Webkit
 * @param {Object} transformStyleName
 * @param {Object} originStyleName
 */
function Transformer(transformStyleName, originStyleName){
    
	this.setTransformMatrix = function(el, matrix){
		if (!(matrix instanceof TMatrix)){
			matrix = TMatrix.describedAs (matrix);
		}		
		
        var items = matrix.items(), m = [items[0][0], items[1][0], items[0][1], items[1][1]];
        el.style[transformStyleName] = 'matrix(' + m.join(',') + ',0,0)';
    };
	
    this.setTransformOrigin = function(el, origin){
        el.style[originStyleName] = origin;
    };
	
    this.getTransformOrigin = function(el){
        return el.style[originStyleName];
    };
	
    this.setMatrix = function(el, matrix){
		el.tMatrix = matrix;
    };
	
    this.getMatrix = function(el){
        return el.tMatrix || TMatrix.Identity();
    };
}

/**
 * For IE :)
 */
function Decepticon(originStyleName){

    Transformer.call(this, '', originStyleName);
    
    /**
     * Create a parent element that has positioning.
     * This way by absolutely positioning the element
     * it will be positioned relative to this element
     * that renders in the same amount of space
     */
    function getTransformParent(el){
        var pNode = el.parentNode, re = /\bie-filter-parent\b/;
        if (re.test(pNode.className)) {
            return pNode;
        }
        //BUG, if for some reason you wanted to transform
        //a span nested within a span, or inline element containing
        //an inline element, or a table row this will fail.
        //basically if your targeted element's parent does not allow divs
        //as children
        var pNode = document.createElement('div'), s = pNode.style, elStyle = el.style;
        
        //set the class name
        pNode.className = 'ie-filter-parent';
        //size the parent to the same dimensions
        s.width = el.offsetWidth + 'px';
        s.height = el.offsetHeight + 'px';
        
        //size the element, because if it's position is not
        //absolute yet, it's rendering will be controlled
        //by its children.  We want to use the computed style here
        function style(str){
            return el.currentStyle[str];
        }
        function pxls(str){
            return parseInt(style(str) || '0', 10);
        }
        function borderWidth(side){
            return 'none' === style('border' + side + 'Style') ? 0 : pxls('border' + side + 'Width');
        }
        
        elStyle.width = (el.offsetWidth - borderWidth('Left') - borderWidth('Right') - pxls('paddingLeft') - pxls('paddingRight')) + 'px', elStyle.height = (el.offsetHeight - borderWidth('Bottom') - borderWidth('Top') - pxls('paddingBottom') - pxls('paddingTop')) + 'px';
        
        var copyThese = ['marginLeft', 'marginTop', 'marginRight', 'marginBottom'];
        if (elStyle.position && elStyle.position.length) {
            switch (elStyle.position) {
                case 'relative':
                case 'absolute':
                case 'fixed':
                    copyThese = copyThese.concat(['left', 'top', 'right', 'bottom']);
                    break;
            }
            
        }
        //copy over any margin
        var l = copyThese.length;
        while (l--) {
            s[copyThese[l]] = elStyle[copyThese[l]];
        }
        pNode.appendChild(el.parentNode.replaceChild(pNode, el));
        
        //default all these
        l = copyThese.length;
        while (l--) {
            elStyle[copyThese[l]] = '';
        }
        elStyle.position = 'absolute';
        
        //null out any refs due to the closures above
        elStyle = null;
        el = null;
        pxls = null;
        return pNode;
    }
    
    function getOriginalDimensions(el){
        var tParent = getTransformParent(el);
        return {
            width: tParent.offsetWidth,
            height: tParent.offsetHeight
        };
    };
    
    //filter property to use for transforms
    var IETransformFilter = 'DXImageTransform.Microsoft.Matrix';
    
    this.setTransformMatrix = function(el, matrix){
		
		if (!(matrix instanceof TMatrix)){
			matrix = TMatrix.describedAs (matrix);
		}
    
        var style = el.style;
        
        if (!el.filters[IETransformFilter]) {
            style.filter = (style.filter ? '' : ' ') + "progid:" + IETransformFilter + "(M11='1.0', sizingMethod='auto expand')";
        }
        
        //get the original dimensions of the element
        //NOTE:::YOU CANNOT USE THE OFFSET WIDTH
        //AND HEIGHT BECAUSE THOSE VALUES CHANGE as the element has the filter applied, 
        //so you must use the originals
        var ogDimensions = getOriginalDimensions(el);
        
        var filter = el.filters[IETransformFilter], mitems = matrix.items();
		mitems = mitems[0].concat(mitems[1]);
        filter.M11 = mitems[0];
        filter.M12 = mitems[1];
        filter.M21 = mitems[3];
        filter.M22 = mitems[4];
        
        
        var w = ogDimensions.width,
		h = ogDimensions.height, 
		origin = OriginParser.getOriginInRelativePixels(this.getTransformOrigin(el), w, h),
		oX = origin.x,
		oY = origin.y;
		YAHOO.log('Original Dimensions: ' + [w,h].join(', '));
       		
		//translate the original center of the element by the negated value of the origin
		//then apply the matrix to this point
		var tX = (w / 2) - oX, tY = (h/2) - oY,
		tItems = [mitems[0] * tX + mitems[1] * tY, mitems[3] * tX + mitems[4] * tY];
		
		//translate back the distance to the transform origin
		//and then subtract the difference from the center of the transformed element
		//the width and height of the bounding box to get the top left hand corner of
		//the bounding box
		var left = Math.floor(tItems[0] + oX - (el.offsetWidth / 2)),
		top = Math.floor(tItems[1] + oY - (el.offsetHeight / 2));
		
		style.left = left + 'px';
		style.top = top + 'px';		
    };

	this.setMatrix = function(el, matrix){
        el.setAttribute('tMatrix', matrix);
    };
}


function createTransformer(){
    
	function autobot(transform, origin){
        return new Transformer(transform, origin);
    }
    
    var elTest = document.createElement('div'),
    standard = autobot('transform', 'transformOrigin');
	
	try{
		/**
		 * Create a transformer based on what
		 * passes the following conditions
		 */
		if (window.CSSMatrix){
			return standard;
		}
		
		if (window.WebKitCSSMatrix){
			return autobot('WebkitTransform', 'WebkitTransformOrigin');
		}
		
		/*moz testing */
		var mozTransformer = autobot('MozTransform', 'MozTransformOrigin');
	    mozTransformer.setTransformMatrix(elTest, {rotate:5});
	    var cssText = elTest.style.cssText;
	            
	    //if the css text contains the browser specific prefix
	    //then we're in a compatible version of FF
	    if (cssText.indexOf('-moz-transform') > -1) {
	    	return mozTransformer;
	    }
		
		//IE Testing
		if (document.body.filters) {
	    	return new Decepticon('transformOrigin');
	    }   
		
	    var transformer;
	    for (var test in transformTests) {
	        if (!transformTests.hasOwnProperty(test)) {
	            continue;
	        }        
	        transformer = transformTests[test](elTest);
	        
	        if (transformer) {
	            return transformer;
	        }
	    }
		
	    //return the web standard, this will do nothing
	    //in all unsupported browsers
	    return standard;
	}
	
	finally{
		elTest = null;
	}	
}
 return createTransformer();
	
}());

/**
 * to add in some pretty syntax
 */
function ElementTransformer(el){
    this.el = el.nodeType ? el : get(el);
}

ElementTransformer.prototype = {
	/**
	 * Uses the element for transforming
	 * @param {Object} el
	 */
    useEl: function(el){
        this.el = el.nodeType ? el : get(el) ;
        return this;
    },
	
	/**
	 * applies the matrix as a transform upon the element
	 * @param {TMatrix} matrix
	 */
    matrix: function(matrix){
        theTransformer.setTransformMatrix(this.el, matrix);
        theTransformer.setMatrix(this.el, matrix);
        return this;
    },
	
	/**
	 * Sets the transform origin on the element
	 * @param {String} origin
	 */
    origin: function(origin){
        theTransformer.setTransformOrigin(this.el, origin);
        return this;
    },
	
	/**
	 * Gets the matrix applied to the element
	 */
    getMatrix: function(){
        return theTransformer.getMatrix(this.el);
    }
};

function elTransform(el){
	return new ElementTransformer(el);
}


var MatrixAnim = (function(){
	
			var matrixAttribute = {
                skewX: 0,
                skewY: 0,
                rotate: 0,
                scale: 0,
                scaleNonUniform: [0, 0], //[x,y]
                rotateFromVector: [0, 0] //[x,y]
            }, 
			lang = YAHOO.lang;
                        
            
            function MatrixAnim(el, attributes, duration, method){
                if (!el) {
                }
                YAHOO.util.Anim.call(this, el, attributes, duration, method);
                
                
                //set the current transforms to apply to the el
                this.transforms = {};
                
                var transformer = new ElementTransformer(YAHOO.util.Dom.get(el));
                
                /**
                 * Changes the animated element
                 * @method setEl
                 */
                this.setEl = function(element){
                    var el = YAHOO.util.Dom.get(element);
                    
                    transformer.useEl(el);
                    
                    //refresh the transforms
                    //if we're working on a new el
                    this.transforms = {};
                };
                
                //subscribe to the ontween method to set the transforms
                this.onTween.subscribe(function(type, args, t){                
                    t.matrix(this.transforms);
                }, transformer, this);               
            }
            
            YAHOO.extend(MatrixAnim, YAHOO.util.Anim, {
            
                setAttribute: function(attr, val){
                    this.transforms[attr] = val;
                },
                
                getAttribute: function(attr){
                    return this.transforms[attr] || matrixAttribute[attr];
                },
                
                doMethod: function(attr, start, end){
                    var method = MatrixAnim.superclass.doMethod, val;
                    if (lang.isArray(matrixAttribute[attr])) {
                        val = [];
                        for (var i = 0, j = start.length; i < j; ++i) {
                            val[i] = method.call(this, attr, start[i], end[i]);
                        }
                    }
                    else {
                        val = method.call(this, attr, start, end);
                    }
                    YAHOO.log(val);
                    return val;
                }
            });
			            
            //set the name
            MatrixAnim.NAME = 'MatrixAnimation';
			
			return MatrixAnim;
}());