
/**
 *
 * javascript arrows based reactivity library
 *
 */

/**
 * Arrows
 */

var __call_depth_limit = 100;
var __call_time_limit = 5;
var __process_id = 0;

var Process = new Class({
    Implements: Events,

    initialize: function(k, x, opt){
        this.k = [k];
        this.x = x;
        this.id = __process_id++;
        this.cancelled = false;
        this.timelimit = __call_time_limit;
        this.call_depth_limit = __call_depth_limit;
        this.parent = null;

        if(opt){ $extend(this, opt); }

    },

    findRootProcess: function(){
         var cur = this;
         while(cur.parent){ cur = cur.parent; }
         return cur;
    },


    saveContext: function(){
        return { k: this.k,
                 x: this.x,
                 cancelled: this.cancelled };
    },

    contWithContext: function(ctx){
        this.k = ctx.k;
        this.x = ctx.x;
        this.cancelled = ctx.cancelled;
        this.cont(this.x);
    },

    start: function(){
        this.trampoline(this.x, this.k.pop(), this.terminal());
        return this;
    },

    cancel: function(){
        if(this.cancelled){ return; }
        this.fireEvent('cancel', [this]);
        this.cancelled = true;
    },

    trampoline: function(x, f, g){
        delete this.cont;
        this.x = x;
        switch(arguments.length){
            case 2: this.k.push(f); break;
            case 3: this.k.push(g, f); break;
        }

        while(true){
            this.calldepth = 0;
            this.start_time = new Date();
            try{
                this.cont( this.x );
            } catch( e ){
                if(e === this){ continue; }
                else { throw e; }
            }
            break;
        }
        this.cont = this.trampoline;
    },

    cont: function(x, f, g){
        if(this.cancelled){ return; }

        if(this.calldepth++ < this.call_depth_limit){
            switch(arguments.length){
                case 0:
                case 1:
                    this.k.pop().k(x, this);
                    break;
                case 2:
                    this.fireEvent('advance', x);
                    f.k(x, this);
                    break;
                case 3:
                    this.k.push(g);
                    this.fireEvent('advance', x);
                    f.k(x, this);
                    break;
            }
        } else {
            switch(arguments.length){
                case 2: this.k.push(f); break;
                case 3: this.k.push(g, f); break;
            }

            if((new Date() - this.start_time) < this.timelimit){
                this.x = x;
                throw this;
            } else {
                var self = this;
                setTimeout( function() { self.cont(x); }, 0);
            }
        }
    },

    terminal: function(){ 
                  var self = this;
                  return { k: function(x, a){ 
                      a.x = x; 
                      self.fireEvent('finish', [self]);
                  } };
    }
});

var ArrowA = new Class({

    initialize: function(k){
        this.k = k;
    },

    A: function(){ return this; },

    _makeProcess: function(x, opt){
        var o = opt || {};
        return new Process(this, x, opt);
    },

    run: function(x, opt){
        var o = opt || {};
        var cont = this._makeProcess(x, o);
        return cont.start().x;
    },

    start: function(x, opt){
        var o = opt || {};
        var cont = this._makeProcess(x, o);
        return cont.start();
    },

    next: function(g){
        g = g.A();
        var f = this;
        return new ArrowA( function(x, k){ k.cont(x, f, g); });
    },

    nth: function(n, a){
        if(arguments.length === 1){
            //return function(arr){ return arr[n]; }.A().next( this );
            var self = this;
            return new ArrowA(function(x, k){
                    var ret = $A(x); //copy array
                    var subProcess = self._makeProcess( ret[n], {parent: k});

                    function join(){ ret[n] = subProcess.x; k.cont( ret ); }
                    function onCancel(){ k.cancel(); }
                    subProcess.addEvents({'cancel': onCancel, 'finish': join}).start();
                });
        } else {
            return this.next( a.A().nth(n) );
        }
    },

    first: function(g){ return this.nth(0, g); },

    second: function(g){ return this.nth(1,g); },

    __makeParAndWait: function(fns, unpacker){
        if(!fns.length){ return this; }
        if(fns.length === 1){ return this.next( fns(0) ); }

        return this.next( new ArrowA(function(x, k){
                    var running = fns.length;
                    var cps = fns.map(function(f, i){ return f.A()._makeProcess(unpacker(x,i),
                                                                                {parent: k}); });

                    function join(){
                        if(--running === 0){
                            k.removeEvent('cancel', onSelfCancel);
                            k.cont( cps.map(function(c){ 
                                    c.removeEvent('cancel', onCancel);
                                    c.removeEvent('finish', join);
                                    return c.x; 
                            }) );
                        }
                    }

                    function onCancel(){
                        cps.forEach(function(c){ 
                            c.removeEvent('cancel', onCancel);
                            c.cancel(); 
                        });
                        k.removeEvent('cancel', onSelfCancel);
                        k.cancel();
                    }

                    function onSelfCancel(){
                        k.removeEvent('cancel', onSelfCancel);
                        cps.forEach(function(c){ 
                                c.removeEvent('cancel', onCancel);
                                c.cancel(); 
                        });
                    }

                    k.addEvent('cancel', onSelfCancel);
                    for(var i=0; i < cps.length; i++){
                        cps[i].addEvents({'cancel': onCancel, 'finish': join}).start();
                    }
                }));
    },

    fanout: function(){
        var a = $A(arguments);
        if(a.length === 1 && $type(a[0]) === 'array'){ a = a[0]; }
        return this.__makeParAndWait( a, $id );
    },

    par: function(){
        var a = $A(arguments);
        if(a.length === 1 && $type(a[0]) === 'array'){ a = a[0]; }
        return this.__makeParAndWait( a, function(arr, i){ return arr[i]; });
    },

    or: function(g){
        var f = this;
        g = g.A();

        return new ArrowA(function(x, k){
            var p1 = f._makeProcess(x, {parent: k});
            var p2 = g._makeProcess(x, {parent: k});

            //join threads and continues current one
            function join(p){
                if(p === p1){ p2.cancel(); } 
                else{ p1.cancel(); }

                k.removeEvent('cancel', onSelfCancel);
                k.cont( p.x );
            }

            //if f and g are cancelled, propagate cancel to current thread
            var running = 2;
            function onCancel(){
                if(--running === 0){ k.cancel(); }
            }

            //if k is cancelled -> cancel sub processes
            function onSelfCancel(){
                p1.removeEvent('cancel', onCancel);
                p2.removeEvent('cancel', onCancel);
                k.removeEvent('cancel', onSelfCancel);

                p1.cancel();
                p2.cancel();
            }

            k.addEvent('cancel', onSelfCancel);
            p1.addEvents({'cancel': onCancel, 'finish': join});
            p2.addEvents({'cancel': onCancel, 'finish': join});

            p1.start();
            p2.start();
        });
    },

    //like do ... while(pred)
    untilA: function(predA){
        var loop = this.fanout( $id, predA )
                       .next(new ArrowA(function(x, k){
                            if(!x[1]){ k.cont(x[0], loop); }
                            else{ k.cont(x[0]); }
                        }));
        return loop;
    },

    whileA: function( predA, arrowA ){
        /*return this.next( arrowA.A().untilA( predA.A().next(function(x){ 
                            if(x){ return false; }
                            else{ return true; }
                        })));
                        */
        var loop = this.fanout( $id, predA )
                       .next(new ArrowA(function(x,k){
                           if(x[1]){ k.cont(x[0], arrowA.A().next(loop)); }
                           else{ k.cont(x[0]); }
                       }));
        return loop;
    },

    loop: function(){
        return this.untilA( function(){ return false; } );
    },

    filter: function( /* preds... */ ){
        var fns = [$id].concat( $A(arguments) );
        var loop = this.next(function(d){ return [d].concat(d); })
                       .par( fns )
                       .next( new ArrowA( function(p, k){
                                   var x = p.shift();
                                   if(p.every(function(x){ return x ? true : false; })){
                                       k.cont( x );
                                   } else {
                                       k.cont( x, loop );
                                   }
                       }));
        return loop;
    }
});

function $whileA(predA, arrowA){
    return $id.A().whileA( predA, arrowA );
}


Function.implement({

    A: function(){
        var f = this;
        return new ArrowA(function(x, k){ k.cont( f(x) ); });
    }

});

