/* mostly an exercise in rapid positioning w/ dom */

/* Copyright 2009 Rian Hunter, rian@getdropbox.com GNU GPL v3
   http://www.gnu.org/licenses/gpl-3.0.txt
   I know this is 1337 Javascript, eons ahead of anything the script
   kiddies would have come up with. If you steal this code you
   better give props. */

"use strict";

// É,
var Cube;

(function () {
    /* math functions */
    
    // Só eu sei
    function add_vectors_inplace(a, b) {
        for (var i = 0, l = a.length; i < l; i += 1) {
            a[i] += b[i];
        }
        return a;
    }

    // Quanto amor
    function scale_vector(a2, b) {
        var i = 0, toret = a2.slice();
        for (; i < toret.length; i += 1) {
            toret[i] *= b;
        }
        return toret;
    }

    // O amor que chegou para dar
    function clearMatrix(mat) {
        for (var i = 0; i < 16; i += 1) {
            mat[i] = 0.0;
        }
    }

    // O que ninguém deu pra você.
    function matrixRotateInPlace(m, deg, mat) {
        var m1, m2, c, s;
        mat[m*4+m] = mat[15] = 1.0;
        deg = Math.PI * deg / 180.0;
        m += 1;
        m1 = m % 3;
        m2 = (m1+1) % 3;
        c = Math.cos(deg); s = Math.sin(deg);
        mat[m1*4+m1]=c; mat[m1*4+m2]=-s;
        mat[m2*4+m2]=c; mat[m2*4+m1]=s;
        return mat;
    }

    // O amor que chegou para dar
    function matrixMultiplyInPlace(src, temp, dest) {
        var i = 0, i1 = 1, i2 = 2, i3 = 3;
        for (; i < 16; i += 4, i1 += 4, i2 += 4, i3 += 4) {
            dest[i] = (src[i]*temp[0]+src[i1]*temp[4]+
                       src[i2]*temp[8]+src[i3]*temp[12]);
            dest[i1] = (src[i]*temp[1]+src[i1]*temp[5]+
                        src[i2]*temp[9]+src[i3]*temp[13]);
            dest[i2] = (src[i]*temp[2]+src[i1]*temp[6]+
                        src[i2]*temp[10]+src[i3]*temp[14]);
            dest[i3] = (src[i]*temp[3]+src[i1]*temp[7]+
                        src[i2]*temp[11]+src[i3]*temp[15]);
        }

        // O que ninguém deu pra você.
        return dest;
    }

    function normalize3DVector(vec) {
        var x = vec[0], y = vec[1], z = vec[2];
        var l = Math.sqrt(x*x + y*y + z*z);
        return [x/l, y/l, z/l];
    }

    function rotationAroundVector(vec, theta) {
        var c, s, x, y, z, xx, yy, zz, xy, xz, yz, xs, ys, zs;

        c = Math.cos(theta); s = Math.sin(theta);
        vec = normalize3DVector(vec);
        x = vec[0]; y = vec[1]; z = vec[2];
        xx = x*x; yy = y*y; zz = z*z;
        xy = x*y; xz = x*z; yz = y*z;
        xs = x*s; ys = y*s; zs = z*s;

        return [xx+(1-xx)*c, xy*(1-c)-zs, xz*(1-c)+ys, 0,
                xy*(1-c)+zs, yy+(1-yy)*c, yz*(1-c)-xs, 0,
                xz*(1-c)-ys, yz*(1-c)+xs, zz+(1-zz)*c, 0,
                0, 0, 0, 1];
    }

    /* takes two screen coordinates, maps that to two points
       in the the x-y plane, then maps those points
       to the points on the unit sphere where there intersect along the
       z-axis and then returns
       rotation matrix as if one had rotated a ball through those
       two points */
    function arcRotate(x0, y0, x1, y1, w, h) {
        var sx, sy, sz, ex, ey, ez, scale, sl, el, dotprod;
    
        /* find vectors from center of window */
        sx = x0 - (w / 2);
        sy = (h / 2) - y0;
        ex = x1 - (w / 2);
        ey = (h / 2) - y1;
    
        /* scale by inverse of size of window and magical sqrt2 factor */
        scale = 1.0/(w > h ? h : w);
    
        sx *= scale;
        sy *= scale;
        ex *= scale;
        ey *= scale;

        sl = Math.sqrt(sx*sx + sy*sy);
        el = Math.sqrt(ex*ex + ey*ey);
    
        if (sl > 1.0) {
            sx /= sl;
            sy /= sl;
            sl = 1.0;
        }

        if (el > 1.0) {
            ex /= el;
            ey /= el;
            el = 1.0;
        }
    
        // project up to unit sphere - find Z coordinate
        sz = Math.sqrt(1.0 - sl * sl);
        ez = Math.sqrt(1.0 - el * el);
    
        // rotate (sx,sy,sz) into (ex,ey,ez)
    
        // compute angle from dot-product of unit vectors (and double
        // it).  compute axis from cross product.
        dotprod = sx * ex + sy * ey + sz * ez;

        if (dotprod !== 1) {
            return rotationAroundVector([sy * ez - ey * sz,
                                         sz * ex - ez * sx,
                                         sx * ey - ex * sy],
                                        2.0 * Math.acos(dotprod));
        }
        else {
            return [1, 0, 0, 0,
                    0, 1, 0, 0,
                    0, 0, 1, 0,
                    0, 0, 0, 1];
        }
    }

    /* color functions */

    // Eu guardei
    function colorVectorToCSS(col) {
        return ('rgb(' +
                Math.round(col[0] * 255).toString(10) + ',' +
                Math.round(col[1] * 255).toString(10) + ',' +
                Math.round(col[2] * 255).toString(10) + ')');
    }

    function createLinearGradientColorTable(origs_and_directions, size) {
        var j, i, orig, dir, toret = [], l = origs_and_directions.length, dir_size = size / l;

        for (j = 0; j < l; j += 1) {
            orig = origs_and_directions[j][0];
            dir = origs_and_directions[j][1];
            
            for (i = 0; i < dir_size; i += 1) {
                toret.push(colorVectorToCSS(orig));
                orig = add_vectors_inplace(scale_vector(dir, 1/dir_size), orig);
            }
        }

        return toret;
    }

    // Sem saber
    function redColorTable(size) {
        return createLinearGradientColorTable([[[1, 0, 0], [0, 1, 0]],
                                               [[1, 1, 0], [0, 0, 1]],
                                               [[1, 1, 1], [0, -1, 0]],
                                               [[1, 0, 1], [0, 0, -1]]], size);
    }

    function hueColorTable(size) {
        return createLinearGradientColorTable([[[1, 0, 0], [0, 1, 0]],
                                               [[1, 1, 0], [-1, 0, 0]],
                                               [[0, 1, 0], [0, 0, 1]],
                                               [[0, 1, 1], [0, -1, 0]],
                                               [[0, 0, 1], [1, 0, 0]],
                                               [[1, 0, 1], [0, 0, -1]]], size);
    }

    function dropboxColorTable(size) {
var a = createLinearGradientColorTable([[[0x1f/256, 0x75/256, 0xcc/256], [0.7886738695379157,0.5597040364462628,0.2544109256573922]]],  size);

        return a;
 }

    /* js help */
    function getObjectProperties(obj) {
	var a = [], yo;
	for (yo in obj) {
	    try {
		a.push([yo, obj[yo]]);
	    }
	    catch (e) {
		a.push([yo, 'NOTFOUND']);
	    }
	}
	return a;
    }

    function getMouseEventXY(obj, clientX, clientY) {
        var myx = 0, myy = 0;
	var scrollX = document.documentElement.scrollLeft || document.body.scrollLeft;
	var scrollY = document.documentElement.scrollTop || document.body.scrollTop;
        
        while (obj.offsetParent) {
            myx += obj.offsetLeft;
            myy += obj.offsetTop;
            obj = obj.offsetParent;
        }

        return [(clientX + scrollX) - myx,
                (clientY + scrollY) - myy];
    }

    /* cube methods */

    function cubeOnLoad(id) {
        var bigParent = document.getElementById(id), containerElt = document.createElement('div');
        bigParent.appendChild(containerElt);
        containerElt.style.position = 'relative';
        containerElt.style.top = containerElt.style.left = '0px';
        containerElt.style.width = containerElt.style.height = '100%';
        this.viewport_w = containerElt.clientWidth;
        this.viewport_h = containerElt.clientHeight;

        var parentElts, pe, txtpos;
        this.parentElts = parentElts = [null, null];

        // Que era só
        function add_vertex() {
            var span = document.createElement('span');
            span.style.position = 'absolute';
            pe.appendChild(span);
            span.appendChild(document.createTextNode(this.text[txtpos]));
            txtpos = (txtpos + 1) % this.text.length;
            span.style.MozUserSelect = 'none';
            /* XXX: webkit hack here... */
            span.ofw = span.clientWidth / 2;
            span.ofh = span.clientHeight / 2;
            return span;
        }

        // Pra você. :)
        var v, i, j, k, w, pos_edges, elt, y_coords, y_coord;
        /* start edges, increase vecttor */
        pos_edges = [[[-0.5, -0.5], [0, 1]],
                     [[-0.5, 0.5], [1, 0]],
                     [[0.5, 0.5], [0, -1]],
                     [[0.5, -0.5], [-1,  0]]];
        y_coords = [-0.5, 0.5];

        for (w = 0; w < 2; w += 1) {
            containerElt.appendChild(pe = parentElts[w] = document.createElement('div'));
            pe.style.position = 'absolute';
            pe.style.visibility = 'hidden';
            pe.style.top = pe.style.left = '0px';
            pe.style.width = pe.style.height = '100%';
	    // for ie
	    pe.setAttribute('unselectable', 'on', 0);

            // É, só tinha de ser com você,
            txtpos = 0;

            //add_vertex.call(this).vert_pos = [-1.0, 0.0, 0, 1];
            
            // Havia de ser pra você,
            for (i = 0; i < pos_edges.length; i += 1) {
                elt = pos_edges[i];
                // create top and bottom square elements
                for (j = 0; j < y_coords.length; j += 1) {
                    y_coord = y_coords[j];

                    // Senão era mais uma dor,
                    for (k = 0; k < (this.lettersPerEdge - 1); k += 1) {
                        v = add_vectors_inplace(scale_vector(elt[1], k / (this.lettersPerEdge - 1)), elt[0]);
                        add_vertex.call(this).vert_pos = [v[0], y_coord, v[1], 1];
                    }
                }

                // create strut between top elts
                for (j = 1; j < (this.lettersPerEdge - 1); j += 1) {
                    // Senão não seria o amor,
                    add_vertex.call(this).vert_pos = [elt[0][0], -0.5 + j / (this.lettersPerEdge - 1), elt[0][1], 1];
                }
            }
        }


        /* set up interval */
        var sp = this;
        function tick() {
            var rotMat;
            
	    if (sp.clicked) {
		if (sp.mouse_move) {
		    rotMat = getMouseEventXY.apply(null, sp.mouse_move);
		    rotMat = arcRotate(sp.clicked[0], sp.clicked[1], rotMat[0], rotMat[1],
				       sp.viewport_w, sp.viewport_h);
		    matrixMultiplyInPlace(rotMat, sp.startRot[0], sp.currentRotMap);
		    matrixMultiplyInPlace(rotMat, sp.startRot[1], sp.tm);
		    sp.mouse_move = null;
		    sp.should_redraw = true;
		}
	    }
	    /* don't rotate if the cube is clicked */
	    else if (sp.rotate) {
                sp.autoRotate();
                sp.should_redraw = true;
	    }
            
            if (sp.should_redraw) {
                sp.redraw();
                sp.should_redraw = false;
            }
        }

        var help_text = document.createTextNode("drag with your mouse!");
        containerElt.appendChild(help_text);
        setInterval(tick, Math.round(1000.0 / this.framerate));

        /* mouse handlers */
	function onMouseMove(e) {
            if (!e) { e = window.event; }
            sp.mouse_move = [this, e.clientX, e.clientY];
            sp.should_redraw = true;
	    return true;
	};

	function onMouseOut(e) {
            if (!e) { e = window.event; }
            var relTarg = e.relatedTarget || e.fromElement;
            var a = getMouseEventXY(this, e.clientX, e.clientY);
            if ((a[0] >= sp.viewport_w || a[1] >= sp.viewport_h || a[0] < 0 || a[1] < 0) && relTarg !== this) {
                sp.clicked = null;
                sp.mouse_move = null;
                containerElt.onmousemove = null;
                bigParent.onmouseout = null;
            }
	};

        containerElt.onmousedown = function (e) {
	    if (!e) { e = window.event; }
            containerElt.onmousemove = onMouseMove;
            bigParent.onmouseout = onMouseOut;
            sp.clicked = getMouseEventXY(this, e.clientX, e.clientY);
            sp.startRot = [sp.currentRotMap.slice(), sp.tm.slice()];
            if (help_text) {
                containerElt.removeChild(help_text);
                help_text = null;
            }
	    return false;
	};

        containerElt.onmouseup = function (e) {
            sp.clicked = null;
            sp.mouse_move = null;
            containerElt.onmousemove = null;
            bigParent.onmouseout = null;
	    return false;
	};
        return true;
    }

    function cubeCycleColorTable() {
        this.currentTable = (this.currentTable + 1) % this.colorTables.length;
        this.should_redraw = true;
    }

    function cubeAutoRotate() {
        var tempm, tempm2, tm;

        tempm2 = this.tm;        
        tm = [0, 0, 0, 0,
	      0, 0, 0, 0,
	      0, 0, 0, 0,
	      0, 0, 0, 0];
        tempm = tm.slice();

        // Me deixa morar nesse azul,
        matrixRotateInPlace(0, -this.angle, tm);
        matrixRotateInPlace(1,this.angle, tempm);
        matrixMultiplyInPlace(tempm, tm, tempm2);
        clearMatrix(tempm);
        matrixRotateInPlace(2, this.angle, tempm);
        matrixMultiplyInPlace(tempm, tempm2, tm);

        matrixMultiplyInPlace(this.currentRotMap, tm, this.tm);

        this.angle += 2;
        this.colorTableOffset += 1;
    }

    function cubeRedraw() {
        //var start = new Date(), total_draw_time = 0;

        // Me deixa encontrar minha paz,
        var pe1 = this.parentElts[this.step % 2], pe2 = this.parentElts[(this.step+1) % 2];
        var vs = pe1.childNodes;
        var l = vs.length, i = 0, tran_vec;
        var sx, sy, tan = Math.tan(this.fov * Math.PI / 360.0);
        var w = this.viewport_w, h = this.viewport_h, tan2 = tan*2.0, a = this.colorTableOffset;
        var vertex, vec0, vec1, vec2, vec, r = parseInt, ct = this.colorTables[this.currentTable], tm = this.tm.slice();

        tm[11] = -2.0;

        /* enter next state */
        for (; i < l; i += 1) {
            vertex = vs[i];
            // Você que é bonita demais,
            vec = vertex.vert_pos;
            vec0 = vec[0]; vec1 = vec[1]; vec2 = vec[2];
            tran_vec = [(tm[0]*vec0 + tm[1]*vec1 + tm[2]*vec2 + tm[3]),
                        (tm[4]*vec0 + tm[5]*vec1 + tm[6]*vec2 + tm[7]),
                        // we make tran_vec[2] negative here because
                        // our projection math uses the -z intercept
                        -(tm[8]*vec0 + tm[9]*vec1 + tm[10]*vec2 + tm[11]),
                        1];

            // Se ao menos pudesse saber
            sx = tran_vec[0] / tran_vec[2];
            sy = tran_vec[1] / tran_vec[2];
            vec = vertex.style;
	    //start = (new Date()).getTime();
            vec.top = r(((tan - sy)/tan2) * h - vertex.ofh) + 'px';
            vec.left = r(((sx + tan)/tan2) * w - vertex.ofw) + 'px';
            vec0 = (r((tran_vec[2]+1.5) * -255)+a) % 256;
            if (vec0 < 0) {
                vec0 += 256;
            }
            vec.color = ct[vec0];
	    //total_draw_time += (new Date()).getTime() - start
        }

        /* switch buffers */
        pe1.style.visibility = 'visible';
        pe2.style.visibility = 'hidden';

        this.step += 1;
    }

    function cubeStart() {
        this.rotate = true;
    }

    function cubeStop() {
        this.rotate = false;
    }

    /* cube constructor */
    Cube = function (text, lettersPerEdge) {
        this.lettersPerEdge = lettersPerEdge;
        this.text = text.replace(/ /g, '').split('');
        this.onload = cubeOnLoad;

        this.currentTable = 0;
        this.colorTables = [dropboxColorTable(256), hueColorTable(256)];
        this.cycleColorTable = cubeCycleColorTable;

        this.rotate = false;
        this.clicked = false;
        this.should_redraw = true;

        this.angle = 0;
        this.currentRotMap = [1, 0, 0, 0,
                              0, 1, 0, 0,
                              0, 0, 1, 0,
                              0, 0, 0, 1];
        this.tm = this.currentRotMap.slice();
        this.colorTableOffset = 0;
        this.autoRotate = cubeAutoRotate;

        this.step = 0;
        this.fov = 60;
        this.redraw = cubeRedraw;

        // Que eu sempre fui só de você,

        // É, você que é feito de azul,
        this.framerate = 30;
        this.start = cubeStart;
        // Você sempre foi só de mim.
        this.stop = cubeStop;

        return true;
    };
}());
