/* $Id: world.js 176 2010-08-10 17:45:11Z victor $ */


/*
    Animated 2d world - render shapes on an HTML canvas.
    
    The term 'step' is used to indicate a slice of
    time in the draw engine. Sprites are moved
    according to animators that act on them in a given
    time.
    
    Callers use .Run() to automate the events.
    
    There are two properties to control the automation:
    
        Running([boolean])
        Stepping([boolean])
    
    Call these without a parameter to get the current state,
    or with a boolean to set the state. (Stepping is useful
    when debugging, probably not much else.) N.B. when calling
    the property setter version, the return value is the
    PREVIOUS state, not the current one.

    If you turn on Stepping() then you are responsible for
    calling Run() to advance the state of the dynamics
    and to render the results.
    
    Again, most useful for a 'Step' button your testbed:
    
        $('button #step).click( function() {
            
            world.Stepping(true);
            world.Run();
        })
    
    Callers can register callbacks that are sync'd to
    the Run() operation. This allows for animation frames
    to happen before and/or after each collision
    detection step. 

    Callers can decide whether they want their animation
    to happen before or after the collision step.
    
        world.Bind('preStep', callback, callbackThis, [,fps [,data]])
        world.Bind('postStep', callback, callbackThis, [,fps [,data]])

    The optional fps parameter is used to decide how often
    to invoke the animation callback. N.B. the actual
    number of times the callback is invoked per second
    will never be larger than the world frame step interval
    as set in the world's FPS property.
    
    The return value of these methods is a token which
    can later be used to remove the callbacks from
    the animation phase
    
        world.Unbind('preStep', token );
        world.Unbind('postStep', token );
    
    Callers can also a one-time only callback that is
    synchonized with the physics step:
    
        world.Bind('stepQueue', callback, callbackThis )
        
    The callback fun will be called the very next step
    iteration, then will be forgotten, never run again.
    This can be useful for timing of resource de-
    allocation (like safely destroying sprites). Example
    usage:
    
        function myFunc(world,sprite)
        {
            // do something here
            
            world.Bind('stepQueue', this.DoSomething, this );
            
            // pretend like 'sprite' was destroyed
            // even though it may not be for a
            // few milliseconds in another thread
        }
        
    See AnimationController for how to animate actions and frames
*/

var tooDeeCollider = function()
{
}

tooDeeCollider.prototype = {
    bounds: [],
    dirty: true,
    
    Add: function(sprite)
    {
        function makeNode(sprite,x,islo)
        {
            return {
                sprite: sprite,
                x: x,
                islo: islo
            };
        };

        var bnds = sprite.dimensions,
            at = this.bounds.length;
        
        this.bounds.push( makeNode( sprite, bnds.UL.x, true ) );
        this.bounds.push( makeNode( sprite, bnds.LR.x, false ) );
        
        sprite.boundsRange = { lo: at++, hi: at }
        
        this.dirty = true;
    },

    Remove: function(sprite)
    {
        if( !sprite.boundsRange )
            return;
        
        // order dependent:
        this.bounds.splice(sprite.boundsRange.hi,1);
        this.bounds.splice(sprite.boundsRange.lo,1);
        delete sprite.boundsRange;
        this.dirty = true;
    },
    
    Replace: function(sprite)
    {        
        var bnds = sprite.dimensions,
            br = sprite.boundsRange,
            B = this.bounds;

        if( B[br.lo].sprite != sprite ||
            B[br.hi].sprite != sprite )
        {
            throw Error("Collisions messed up");
        }
        B[br.lo].x = bnds.UL.x,
        B[br.hi].x = bnds.LR.x;
        this.dirty = true;
    },
    
    // returns 'true' if bounds were clean already
    // returns 'false' if bounds were dirty and a check() should be performed
    Clean: function()
    {
        if( !this.dirty )
            return true;

        var B = this.bounds;
        
        B.sort( function(a,b) { return a.x == b.x ? (a.islo ? -1 : 1) : a.x - b.x; });

        var len = B.length;

        for( var i = 0; i < len; i++ )
        {
            var b = B[i];
            if( b.islo )
            {
                b.sprite.boundsRange = { lo: i };
            }
            else
            {
                var br = b.sprite.boundsRange;
                B[br.lo].hi = br.hi = i;
            }
        }

        this.dirty = false;
        return false;
    },
    
    // return true means either:
    //   - a collision was found and reported
    //   - the collision has changed during Check()
    // in either case, the world should stop looking for further collisions
    //
    // return false means no hit was found and
    // nothing changed in the collision list
    //
    Check: function(sprite)
    {   
        var lo = sprite.boundsRange.lo,
            hi = sprite.boundsRange.hi,
            hits = [];
        
        for( var i = lo+1; i < hi; i++ )
        {
            hits.push(i);
        }
    
        while(lo)
        {
            --lo;
            var b = this.bounds[lo];
            if( b.islo && b.hi > hi )
                hits.push(lo);
        }

        if( hits.length )
        {
            var sbnds = sprite.dimensions,
                y1 = sbnds.UL.y,
                y2 = sbnds.LR.y;
            
            for( i = 0; i < hits.length; i++ )
            {
                var s = this.bounds[hits[i]].sprite;
        
                var bnds = s.dimensions,
                    t1 = bnds.UL.y,
                    t2 = bnds.LR.y,
                    hit = (y1 <= t1 && t1 < y2) ||
                          (t1 < y1 && y1 < t2) ||
                          (t1 < y1 && t2 > y2);
                          
                if( hit )
                {
                    sprite.CollideNotify.apply(sprite,[s]);
                    return true;
                }
            }
        }
        
        return this.dirty;
    }
};



var tooDeeWorld = function(options)
{    
    var defaults = new function() {
        this.pixelPerMeter = 30;
        this.fps = 12;
        this.clockResolution = 36;
        this.canvasId = 'CANVAS';
        this.slopFactor = 0.005;
    };

    this.config = tooDeeOptMix( {}, defaults, options || {});

    var ppm = this.config.pixelPerMeter;
    var mpp = this.config.meterPerPixel = 1 / ppm;

    this.config.slop = 1; // this.config.slopFactor * ppm;
    this.config.scaling = { x: ppm, y: ppm }
        
    this.canvasWrapper = new function(canvasId,mpp) 
                        {
                            this.canvas    = document.getElementById(canvasId);
                            this.context   = this.canvas.getContext('2d');
                            this.pixWidth  = parseInt(this.canvas.width);
                            this.pixHeight = parseInt(this.canvas.height);
                            this.width     = this.pixWidth*mpp;
                            this.height    = this.pixHeight*mpp;
                        }(this.config.canvasId,mpp);

    
    this.viewport = {
        x: 0, y: 0
    }
    
    if( ppm != 1 )
        this.canvasWrapper.context.scale( this.config.scaling.x, this.config.scaling.y );
    
    tooDeeEventDelegator.apply(this);
    
    //this.Bind( 'postStep', this.Draw, this, this.config.clockResolution / this.config.fps );

};

tooDeeMixin(tooDeeWorld, tooDeeEventDelegator, {

    running: true,
    stepping: false,
    drawGrid: false,
    drawShapes: true,
    drawWire: false,
    drawBounds: false,
    viewport: {},
    keyHandlers: [],
    sprites: new tooDeeList(),
    movingSprites: new tooDeeList(),
    collider: new tooDeeCollider(),
    zOrderList: [],
    zOrderDirty: false,
    scroller: null,
    inRun: false,
    characters: null,
    background: '#b6b600',
    needUpdate: true,
    
    Sprites: function(func) {
        return this.sprites.Each(func);
    },
    
    KEY_CODES: {
        left: 37,
        up: 38,
        right: 39,
        down: 40,
        ponder: 1000,
        toetap: 1001,
        turnRC: 1002,    // turn right to center
        turnLC: 1003,     // turn left to center
        watchlk: 1004
    },
    
    CHARACTER_CODES: {
        ponder1: 0,
        ponder2: 1,
        ponder: 2,
        turnToLeft: 3,
        turnToRight: 4,
        walkRight: 5,
        walkLeft: 6,
        walkToward: 7,
        walkAway: 8
    },
    
    GENDER: {
        female: 0,
        male: 1
    },

    InitGame: function()
    {
        this._initBoundries();
    },
    
    _initBoundries: function()
    {
        var me = this;
        
        function createBoundry(x,y,w,h,dir)
        {
            var dimensions = new tooDeeDimensions();
            dimensions.Set( x, y, x+w, y+h );
    
            var sprite = new tooDeeViewportBoundry( {
                name: 'boundry.' + dir,
                dimensions: dimensions
            });
            
            me.AddSprite( sprite );
            
            return sprite;
        }
        
        var cW = this.canvasWrapper.width;
        var cH = this.canvasWrapper.height;
    
        this.boundries = [];
    
        this.boundries.push( createBoundry(      0,      0,   cW,   0.05, 'top' ) );
        this.boundries.push( createBoundry(    0.1, cH-0.5,   cW,   0.05, 'bottom' ) );
        this.boundries.push( createBoundry(    0.2,      0, 0.05,     cH, 'left' ) );
        this.boundries.push( createBoundry( cW-0.5,      0, 0.05,     cH, 'right' ) );
    
        // 1 second scroll
        this.scroller = new tooDeeAnimation(this,this.config.fps/2,0,1),
        this.scroller.Freeze(true);        
        this.scroller.Bind('advance', this.TranslateBy, this, 1, [ -1,-1 ] );
        // stop the animation after one cycle
        this.scroller.Bind('cycle', this.scroller.Freeze, this.scroller, 1, [true]);
        
    },
    
    Copy: function()
    {
        return this;
    },
    
    Scale: function( scale ) {
        var oldX = this.config.scaling.x;
        var oldY = this.config.scaling.y;
        
        this.pixelPerMeter = this.config.scaling.x = this.config.scaling.y = scale;
        this.meterPerPixel = 1 / this.pixelPerMeter;
        
        // context scaling is additive,
        // we are not
        this.canvasWrapper.context.scale( scale/oldX, scale/oldY );
    },

    MeterPerPixel: function() {
        return this.config.meterPerPixel;
    },
    
    CanvasToWorld: function( x, y ) {
        var mpp = this.config.meterPerPixel;
        return new tooDeeVec2( x * mpp, y * mpp);
    },

    WorldToCanvas: function( x, y ) {
        var ppm = this.config.pixelPerMeter;
        return new tooDeeVec2( x * ppm, y * ppm );
    },
    
    CanvasDim: function() {
        var cw = this.canvasWrapper;
        return { x: cw.width, y: cw.height };
    },
    
    CreateSprite: function( options ) {
        var spriteClass = options.spriteClass || tooDeeSprite;
        var sprite = new spriteClass(this,options);
        this.AddSprite(sprite);
        return sprite;
    },
    
    AddSprite: function( sprite ) {
        this.sprites.Append(sprite);
        this.SpriteChanged(sprite);
        return sprite;
    },
    
    RemoveSprite: function( sprite ) {
        this.movingSprites.Remove(sprite);
        this._zOrderRemove(sprite);
        this.collider.Remove(sprite);
        this.sprites.Remove(sprite);
    },
    
    SpriteChanged: function( sprite ) {
        if( sprite.visible )
            this._zOrderAdd(sprite);
        else
            this._zOrderRemove(sprite);

        if( sprite.collidable )
            this.collider.Add(sprite);
        else
            this.collider.Remove(sprite);
        
        if( sprite.stationary )
            this.movingSprites.Remove(sprite);
        else 
            this.movingSprites.AppendUnique(sprite);
        
    },
    
    SpriteMoved: function( sprite ) {
        this.collider.Replace(sprite);    
    },
    
    // properties
    
    _prop: function(prop,newval,isConfig) {
        if( !isConfig )
        {
            var old_val = this[prop];
            if( newval != undefined )
                this[prop] = newval;
        }
        else
        {
            var old_val = this.config[prop];
            if( newval != undefined )
                this.config[prop] = newval;            
        }
        return old_val;
    },

    FPS: function(setting) {
        return this._prop('fps',setting,true);
    },

    // Frames per FPS
    FPFPS: function(fps)
    {
        var myFPS = this.config.clockResolution;
        var callerFPS = fps || myFPS;
        return ( callerFPS > myFPS ) ? 1 : Math.round(myFPS / callerFPS);
    },
    
    
    DrawGrid: function(flag) {
        return this._prop('drawGrid',flag);
    },
    
    DrawShapes: function(flag) {
        return this._prop('drawShapes',flag);
    },
    
    DrawWire: function(flag) {
        return this._prop('drawWire',flag);
    },

    DrawBounds: function(flag) {
        return this._prop('drawBounds',flag);
    },

    Running: function(flag) {
        this.stepping = false;
        return this._prop('running',flag);
    },
    
    Stepping: function(flag) {
        this.running = false;
        return this._prop('stepping',flag);
    },
    
    Step: function() {

        var hit = this.Trigger('preStep');
        
        if( !this.tiles || !this.tiles.TestCollision() )
        {
            // Now do shape collision test (person runs into rewards, spouse, etc.)
            var collider = this.collider;
            while( !collider.Clean() )
            {
                this.movingSprites.Each( function(sprite,index,iter) {
                    // not all moving sprites are collidable,
                    // such as hearts and rewards animations
                    if( sprite.collidable && collider.Check(sprite) )
                        iter.Stop();
                });
            }
        }
        
        hit = this.Trigger('stepQueue') || hit;
        this.UnbindAll('stepQueue');
        hit = this.Trigger('postStep') || hit;
        
        this.needUpdate = hit;
        
    },
    
    Run: function() {
        if( this.inRun )
        {
            throw { msg: 'Run steps overlap'};
        }
        
        this.inRun = true;
        
        if( !this.running && !this.stepping )
            return;
    
        this.Step();
        
        if( this.needUpdate )
        {
            this.Draw();
            this.needUpdate = false;
        }
        else
        {
            this.Trigger('idle');
        }

        if( this.running )
        {
            var me = this;
            function runme() { me.Run(); } 
            setTimeout( runme, 1000/this.config.clockResolution);
        }
        
        this.inRun = false;

    },   
        
    GetContext: function() {
        return this.canvasWrapper.context;
    },

    ScrollTranslateBy: function(x,y) {
        if( this.scroller.frozen )
        {
            var delta = this.config.fps; // /4;
            this.scrollAmount = { x: x/delta, y: y/delta };
            this.scroller.Freeze(false);
        }
    },
    
    TranslateBy: function(x,y) {
        if(x==-1)
            {
                x = this.scrollAmount.x;
                y = this.scrollAmount.y;
            }
        this.Translate(this.viewport.x+x,this.viewport.y+y);
    },
    
    Translate: function(x,y) {
        var diff = { x: this.viewport.x - x, y: this.viewport.y - y };
        this.viewport.x = x;
        this.viewport.y = y;

        var len = this.boundries.length;
        for( var i = 0; i < len; i++ )
        {
            var b = this.boundries[i];
            b.dimensions.Offset( diff );
            this.collider.Replace(b);
        }
    },
    
    Draw: function() {
        try {

            var c = this.canvasWrapper;
            if( this.background != 'transparent' )
            {
                c.context.fillStyle = this.background;
                c.context.fillRect( 0, 0, c.width, c.height );
            }
            
            c.context.save();
            
            if( this.viewport.x || this.viewport.y )
            {
                c.context.translate(this.viewport.x,this.viewport.y);    
            }
            
            if( this.drawGrid )
                drawGrid(c,this.config);
    
            if( this.drawShapes || this.drawWire )
            {
                if( this.zOrderDirty )
                    this._zOrderSort();

                for( var spriteIndex in this.zOrderList )
                {
                    var sprite = this.zOrderList[spriteIndex];
                    if( this.drawShapes )
                        sprite.Draw();
                    if( this.drawWire )
                        sprite.DrawWire();
                    if( this.drawBounds )
                        sprite.DrawBounds();
                }
            }
            
            c.context.restore();
        }
        catch(e)
        {
            _log(e);
        }
    
        function drawGrid(canvas,config)
        {
            var context = canvas.context;
            context.save();
            var W = canvas.width;
            var H = canvas.height;
            var mpp = config.meterPerPixel;
            
            context.lineWidth = mpp;
            
            var width = 1.0;
            
            drawAGrid( 0.01, '#FF0000' );
            drawAGrid( 0.51, '#CCFFFF' );
            
            function drawAGrid( start, color  )
            {
                context.save();
                context.strokeStyle = color;
            
                for (var x = start; x < W; x += width ) {
                  context.moveTo(x, 0);
                  context.lineTo(x, H);
                }
                for (var y = start; y < H; y += width ) {
                  context.moveTo(0, y);
                  context.lineTo(W, y);
                }
                context.stroke();
                context.restore();
            }
            
            var ppm = config.pixelPerMeter;
            context.scale(mpp,mpp);
            context.font = "12px sans serif";
            var posX = ppm;
            
            for( var x = 1; x < W; ++x, posX += ppm)
            {
                context.fillText(x,posX-4,ppm/3);
            }
            var posY = ppm;
            for( var y = 1; y < H; ++y, posY += ppm )
            {
                context.fillText(y,ppm/4,posY+3);
            }
            
            context.restore();
        }
    },
    
    // z-order stuff
    
    _zOrderAdd: function(sprite) {
        this._zOrderRemove(sprite); // in case we are renumbering
        this.zOrderList.push(sprite);
        this.zOrderDirty = true;
    },
    
    _zOrderSort: function() {
        this.zOrderList.sort(function(a,b) { return a.zOrder - b.zOrder; });
        this.zOrderDirty = false;
    },
    
    _zOrderRemove: function(sprite) {
        var i = this.zOrderList.indexOf(sprite);
        if( i !== -1 )
            delete this.zOrderList[i];            
    }
    

});



