
function $sequenceA(/* fns... */ ){
    fns = $A(arguments);
    if(!fns.length){ return null; }
    if(fns.length === 1 && $type(fns[0]) === 'array') fns = fns[0];

    var cur = fns[0].A();
    for(var i=1; i < fns.length; i++){
        cur = cur.next(fns[i]);
    }
    return cur;
}

function $foreverA( /*arrows... */ ){
    return $sequenceA($A(arguments)).A().loop();
    //return arrowA.A().loop();
}

function $orA( /* fns... */ ){
    var fns = $A(arguments);
    if(fns.length === 1 && $type(fns[0]) === 'array') fns = fns[0];

    switch(fns.length){
        case 0: return null;
        case 1: return fns[0].A();
        case 2: return fns[0].A().or( fns[1].A() );
    }

    // simple, but needs to much resources
    /* 
    cur = fns[fns.length-1].A();
    for(var i = fns.length-2; i >= 0; i--){
        cur = fns[i].A().or(cur);
    }
    return cur; */

    // better
    return new ArrowA(function(x, k) {
                var ps = fns.map(function(p){ return p._makeProcess(x, {parent: k}) });

                function join(p){
                    //cancel other
                    ps.forEach(function(p2){
                        if(p != p2){ p2.cancel() }
                    });

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

                // if a threads exited, propagate to parent process
                var running = ps.length;
                function onCancel() {
                    if(--running === 0){ k.cancel(); }
                }

                function onSelfCancel(){
                    ps.forEach(function(p){
                            p.removeEvent('cancel', onCancel);
                        });
                    k.removeEvent('cancel', onSelfCancel);
                    ps.forEach(function(p){ p.cancel(); });
                }

                k.addEvent('cancel', onSelfCancel);
                ps.forEach(function(p){
                            p.addEvents({cancel: onCancel, finish: join});
                        });

                ps.forEach(function(p){ p.start(); });
            });
}

function $ifA( predA, thenA, elseA ){
    return $id.A().fanout( $id, predA ).next( new ArrowA(function(x, k){
                if(x[1]){ k.cont( x[0], thenA.A() ); }
                else { k.cont( x[0], elseA.A() ); }
            }));
}

function $whenA(predA, thenA){
    return $ifA(predA, thenA, $id);
}

function $unlessA(predA, thenA){
    return $ifA(predA, $id, thenA);
}

function $constA(x){
    return $const(x).A();
}

function $delayA(delta){
    return new ArrowA(function(x, k){
                var timer;

                function onCancel(){
                    $clear(timer);
                }

                k.addEvent('cancel', onCancel );
                timer = setTimeout(function(){ 
                    k.removeEvent('cancel', onCancel);
                    k.cont(x); 
                }, delta );
            });
}

ArrowA.implement({
    ifA: function(predA, thenA, elseA){
        return this.next( $ifA(predA, thenA, elseA) );
    },

    whenA: function(predA, thenA){
        return this.next( $whenA(predA, thenA) );
    },

    unlessA: function(predA, thenA){
        return this.next( $unlessA(predA, thenA) );
    }
});

function $fanout( /* fns... */){
    return $id.A().fanout( $A(arguments) );
}

function $par( /* fns... */ ){
    return $id.A().par( $A(arguments) );
}

var trueA = function(x){ return true; }.A();
var falseA = function(){ return false; }.A();
function isEqual(x,y){ return x === y; }
function isNull(x){ return x === null; }

function fieldA(name){ 
    return $valueForField.rcurry(name).A();
}

function arefA(i){
    return function(arr){
        return arr[i];
    }.A();
}

/**
 * ignores arrow result and continues with input value
 */
function ignoreResultA(arrow){
    var as = $A(arguments);
    return $fanout( $sequenceA(as), $id ) //duplicate value
            .next( arefA(1) );
}

function isOK(x){ return x ? true : false; }

function setStylesA(styles){
    return function(obj){
        obj.setStyles(styles);
    }.A();
}

var everyA = function(x){ 
    return x && x.every(function(x){
        return x ? true : false;
    });
}.A();

function waitEventWithDelay( evt_arrow, timeout, cancel_evt_arrow ) {
    return function(x){ return { value: x }; }.A()
            .next(
                $sequenceA(
                    fieldA('value'),
                    ignoreResultA( evt_arrow ),
                    $orA( $delayA(timeout).next( function(x){ return {value: x, state: true}; } )
                        , ignoreResultA( cancel_evt_arrow ).next( function(x){ return {value: x, state: false}; } ) )
                    )
                .untilA( fieldA('state') )
                .next( fieldA('value') )
             );
}

function withField( field, arrow ){
    return ignoreResultA( fieldA(field).next( arrow ) );
}

function morphA(transition, opts){
    var params = opts || {};
    return new ArrowA(function(x, k){

            var FX = new Fx.Morph(x, params);

            function onProcessCancel(){
                FX.removeEvent('onCancel', fxCancel);
                FX.cancel();
                FX.removeEvent('onComplete', fxComplete );
            }

            function fxCancel(){
                FX.removeEvent('onCancel', fxCancel);
                FX.removeEvent('onComplete', fxComplete );
                k.removeEvent('cancel', onProcessCancel);
            }

            function fxComplete(){
                FX.removeEvent('onCancel', fxCancel);
                FX.removeEvent('onComplete', fxComplete );
                k.removeEvent('cancel', onProcessCancel);
                k.cont(x);
            }

            k.addEvent('cancel', onProcessCancel);
            FX.addEvents({onCancel: fxCancel, onComplete: fxComplete});
            FX.start( transition );
    });
}

var killA = new ArrowA(function(x, k){
            k.cancel();
        });

function guardA(predA){
    return $unlessA(predA, killA);
}

var flattenA = new ArrowA(function(subProcessA, k){

        var process = subProcessA._makeProcess(null, {parent: k});

        function removeEventListeners(){
            process.removeEvent('cancel', onCancel);
            process.removeEvent('finish', join);
            k.removeEvent('cancel', parentCancel);
        }

        function parentCancel(){
            removeEventListeners();
            process.cancel();
        }

        function join(){
            removeEventListeners();
            k.cont( process.x );
        }


        function onCancel(){
            removeEventListeners();
            k.cancel();
        }

        k.addEvent('cancel', parentCancel);
        process.addEvents({'cancel': onCancel, 'finish': join});
        process.start();
});

function toRecord(field){
    return function(x){
        var tmp = {};
        tmp[field] = x;
        return tmp;
    }.A();
}

// simple, but may block browser on large datasets

/*
function mapA(fn) {
    return function(data){
        return $A(data).map(fn);
    }.A();
}

*/

function mapA(fn) {
    var arg = $A(arguments);
    if(arg.length === 0) return $id
    if(arg.length === 1)
        return $sequenceA(
            function(arr){ return {i:0, arr:arr, result:[]}; },
            $whileA( function(d){ return d.i < d.arr.length; },
                function(d){
                    d.result[d.i] = fn(d.arr[d.i]);
                    d.i++;
                    return d;
                }
            ),
            fieldA('result')
        );

    var fn = arg[1];
    var index = arg[0];

    return $sequenceA(
            function(arr){ return {i:0, arr:arr[index], orig:arr, result:[]}; },
            $whileA( function(d){ return d.i < d.arr.length; },
                function(d) {
                    var param = d.orig.map(function(v, j){
                            if(j === index) return d.arr[d.i];
                            else return v;
                        });
                    d.result[d.i] = fn(param);
                    d.i++;
                    return d;
                }
            ),
            fieldA('result')
        );
}

/*
function eachA(fn) {
    return function(data){
        $A(data).forEach(fn);
    }.A();
}/**/

function eachA(fn){
    var arg = $A(arguments);
    if(arg.length === 0) return $id
    if(arg.length === 1)
        return $sequenceA(
            function(arr){ return {i:0, arr:arr }; },
            $whileA( function(d){ return d.i < d.arr.length; },
                function(d){
                    //console.log('len: ' + d.arr.length + ' i: ' + d.i);
                    fn(d.arr[d.i++]);
                    return d;
                }
            )
        );

    var fn = arg[1];
    var index = arg[0];
    return $sequenceA(
            function(arr){ return {i:0, arr:arr[index], orig:arr }; },
            $whileA( function(d){ return d.i < d.arr.length; },
                function(d) {
                    var param = d.orig.map(function(v, j){
                            if(j === index) return d.arr[d.i];
                            else return v;
                        });
                    fn(param);
                    d.i++;
                    return d;
                }
            )
        );
}

/*
function eachA(fn) {
    return $sequenceA(
        function(arr){ return {i:0, arr:arr}; },
        $whileA( function(d){ return d.i < d.arr.length; },
            function(d){
                fn(d.arr[d.i++]);
                return d;
            }
        ),
    );
}
*/

function filterA(fn){
    return $sequenceA(
        function(arr){ return {i:0, j:0, arr:arr, result:[]}; },
        $whileA( function(d){ return d.i < d.arr.length; },
            function(d){
                if(fn(d.arr[d.i])){
                    d.result[d.j] = d.arr[d.i];
                    d.j++;
                }
                d.i++;
                return d;
            }
        ),
        fieldA('result')
    );
}

function setFieldA(fieldName, constr) {
    return $id.A().fanout( $id, constr ).next( new ArrowA(function(x, k) {
            x[0][fieldName] = x[1];
            k.cont(x[0]);
    }));
}

function setFieldIfUndefinedA(fieldName, constr) {
    var defined = function(x){ return $defined(x[fieldName]); };
    return $whenA( $not( defined ),
                   setFieldA( fieldName, constr));
}

function waitForKill( arrow ) {
    return new ArrowA( function(x, k) {
                function onCancel() {
                    k.removeEvent('cancel', onCancel);
                    arrow.A().run(x);
                }

                k.addEvents({ cancel: onCancel });
            });
}

function createAndRunSub(maker) {
    return function(x) {
        return $constA(x).next( maker(x) );
    }.A().next( flattenA );
}

