/*
 * Trampoline-CPS arrows, using objects instead of closures.
 *
 * Arrows are written according to the following convention:
 *    1. Arrow types are named FooA.
 *
 *    2. Arrows are identified by their identity function FooA.prototype.FooA().
 *
 *    3. Functions are given an (auto)-lifting function Function.prototype.FooA() for each FooA; since all
 *       (single-argument) functions can be lifted into arrows.
 *
 *    4. Arrow constructors are divided into two types:
 *           i.  arrow prototype constructors constructs arrows around their specific function type (usually not used
 *               directly);
 *           ii. arrow constructors which already embed a specific (parameterized) function (typically built with arrow
 *               prototype constructors).
 *       (i.e., arrow prototype constructors are like abstract classes, arrow constructors like concrete classes).
 *
 *    5. Functions lifted via Function.prototype.FooA() are assumed to not know anything about arrows.
 *       Arrows can be constructed from functions via arrow (prototype) constructors. E.g.:
 *             var fA = f.FooA();    // f is just a single-argument function that knows nothing about FooA
 *             var gA = new FooA(g); // g has to conform to FooA's internal function representation
 *
 *    6. Arrow constructors begin with the idiom:
 *           if (!(this instanceof FooA))
 *               return new FooA(eventname);
 *       This allows arrows to be constructed without the new operator.
 *
 *    7. Every binary arrow combinator f.bar(g) begins with the idiom:
 *           g = g.FooA();
 *       This serves two purposes: it performs a dynamic-check on the arrow type (i.e., throws an error if g is
 *       incompatible with f); and it auto-lifts functions to arrows.
 */


/* constructor and signature */
function AsyncA(t) {
    this.t = t;
}
AsyncA.prototype.AsyncA = function() {
    return this;
}
AsyncA.prototype.toString = function() {
    if (!(name in this)) {
        this.name = this.makeName();
    }
    return "[AsyncA " + this.name + "]";
}
AsyncA.prototype.makeName = function() {
    return "anonymous";
}
AsyncA.prototype.run = function(x, opt) {
    return (new AsyncA.Instance(this, x, opt)).progressA;
}



/* bookkeeping object for AsyncA arrow instances */
AsyncA.Instance = function(a, x, opt) {
    /* set up defaults */
    if (opt) {
        if ("calldepthlimit" in opt) {
            this.calldepthlimit = opt.calldepthlimit;
        }
        if ("timelimit" in opt) {
            this.timelimit = opt.timelimit;
        }
    }
    /* continuations */
    this.k = [];
    /* progress */
    this.progressA = new ProgressA(this);
    this.cancellers = [];
    this.observers = [];
    /* state */
    this.env = {};

    /* and start the whole thing */
    this.trampoline(x, a, AsyncA.Instance.Terminal);
}
AsyncA.Instance.Terminal = new AsyncA(function() {});

AsyncA.Instance.prototype.calldepthlimit = 50;
AsyncA.Instance.prototype.timelimit = 30; /* 33 Hz */

AsyncA.Instance.prototype.toString = function() {
    return "[AsyncA.Instance " + this.k + "]"
}
//AsyncA.Instance.prototype.push = function(k) {
//    this.k.push(k);
//    return this;
//}
//AsyncA.Instance.prototype.pop = function(x) {
//    if (this.k.length > 0) {
//        if (this.calldepth++ < AsyncA.calldepthlimit) {
//            /* either continue directly */
//            this.k.pop().t(x, this);
//        } else {
//            /* or thunk */
//            this.x = x;
//            throw this;
//        }
//    }
//}
AsyncA.Instance.prototype.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;
    }
    this.starttime = new Date();
    while (true) {
        this.calldepth = 0;
        try {
            this.cont(this.x);
        } catch (e) {
            if (e === this) {
                /* if we were thunked, continue trampolining */
                continue;
            } else {
                /* it's a real exception! */
                throw e;
            }
        }
        break;
    }
    this.cont = this.trampoline;
}

AsyncA.Instance.prototype.cont = function(x, f, g) {
    if (arguments.length > 3) {
        throw new TypeError("Wrong number of arguments");
    }
    if (this.calldepth++ < this.calldepthlimit) {
        /* either continue directly */
        switch (arguments.length) {
            case 0:
            case 1:
                this.k.pop().t(x, this);
                break;
            case 2:
                f.t(x, this);
                break;
            case 3:
                this.k.push(g);
                f.t(x, this);
                break;
        }
    } else {
        /* or thunk */
        switch (arguments.length) {
            case 2: this.k.push(f); break;
            case 3: this.k.push(g, f); break;
        }
        if ((new Date() - this.starttime) < this.timelimit) {
            this.x = x;
            throw this;
        }
        var self = this;
        setTimeout(function() { self.cont(x); }, 0);
    }
}


/*
 * ProgressA: Arrows for tracking progress of an AsyncA arrow (i.e. progress event handler).
 *
 * Two operations are supported: ProgressA arrows can be composed to handle progress events (arrows calling advance())
 * of their corresponding AsyncA arrows instance; ProgressA arrows can also be used to cancel the entire operation
 * their AsyncA arrows.
 *
 * The internals of ProgressA is actually implemented in AsyncA; ProgressA itself is just the public interface.
 */

function ProgressA(a) {
    if (!(this instanceof ProgressA))
        return new ProgressA(a);
    this.a = a;
}
ProgressA.prototype = new AsyncA(function(x, a) {
    this.a.addObserver(a);
});
ProgressA.prototype.cancel = function() {
    this.a.cancel();
}

/* ProgresssA internal functions */
AsyncA.Instance.prototype.addCanceller = function(canceller) {
    this.cancellers.push(canceller);
}
AsyncA.Instance.prototype.addObserver = function(observer) {
    this.observers.push(observer);
}
AsyncA.Instance.prototype.advance = function(canceller) {
    /* remove canceller function */
    var index = this.cancellers.indexOf(canceller);
    if (index >= 0) {
        this.cancellers.splice(index, 1);
    }
    /* signal observers */
    var observers = this.observers;
    this.observers = [];
    while (observers.length > 0)
        observers.pop().cont(); /* TODO: obey time limit */
}
AsyncA.Instance.prototype.cancel = function() {
    /* cancel all in-progress arrows */
    var cancellers = this.cancellers;
    this.cancellers = [];
    while (cancellers.length > 0)
        cancellers.pop()();
}


/* proxy object for forks; maintains a subtree of cancellers */
//AsyncA.Instance.Fork = function(a) {
//    this.a = a;
//    this.cancellers = [];
//    this.progressA = new ProgressA(this);
//}
//AsyncA.Instance.Fork.prototype.trampoline = function() {
//    this.a.trampoline.apply(this.a, arguments);
//}
//AsyncA.Instance.Fork.prototype.cont = function() {
//    this.a.cont.apply(this.a, arguments);
//}
//AsyncA.Instance.Fork.prototype.addObserver = function(observer) {
//    this.a.addObservers.apply(this.a, arguments);
//}
//AsyncA.Instance.Fork.prototype.advance = function(canceller) {
//    /* remove canceller function */
//    var index = this.cancellers.indexOf(canceller);
//    if (index >= 0) {
//        this.cancellers.splice(index, 1);
//    }
//    this.a.advance.apply(this.a, arguments);
//}
//AsyncA.Instance.Fork.prototype.addCanceller = function(canceller) {
//    this.cancellers.push(canceller);
//    this.a.addCanceller.apply(this.a, arguments);
//}
//AsyncA.Instance.Fork.prototype.cancel = function(canceller) {
//    /* cancel all in-progress arrows */
//    var cancellers = this.cancellers;
//    this.cancellers = [];
//    while (cancellers.length > 0)
//        cancellers.pop()();
//}
//AsyncA.Instance.prototype.fork = function() {
//    return new AsyncA.Instance.Fork(a);
//}





/* thunk objects for functions */
AsyncA.FunctionThunk = function(f) {
    this.f = f;
}
AsyncA.FunctionThunk.prototype = new AsyncA(function(x, a) {
    a.cont(this.f(x, a.env));
});
AsyncA.FunctionThunk.prototype.makeName = function() {
    return /function ([^(]*)/.exec(this.f.toString())[1] || "function";
}
Function.prototype.AsyncA = function() {
    return new AsyncA.FunctionThunk(this);
}


/* thunk object for next combinator */
AsyncA.NextThunk = function(f, g) {
    this.f = f;
    this.g = g;
}
AsyncA.NextThunk.prototype = new AsyncA(function(x, a) {
    a.cont(x, this.f, this.g);
});
AsyncA.NextThunk.prototype.makeName = function() {
    return "(" + this.f.makeName() + ">>>" + this.g.makeName() + ")";
}
AsyncA.prototype.next = function(g) {
    return new AsyncA.NextThunk(this, g.AsyncA());
}
Function.prototype.next = function(g) {
    return this.AsyncA().next(g);
}


/* thunk object for bind combinator */
/* TODO: is this the same as monad bind? or ArrowApply app? */
/* looks like a hybrid of monad bind and ArrowApply app, since it avoids using tuple pairs */
function BindA(f) {
    if (!(this instanceof BindA))
        return new BindA(f);
    this.f = f.AsyncA();
}
BindA.prototype = new AsyncA(function(x, a) {
    a.cont(x, this.f, BindA.ApplyThunk);
});
BindA.ApplyThunk = new AsyncA(function(x, a) {
    a.cont(undefined, x);
});


/*
 * AsyncA.prototype.repeat(): looping combinator.
 *
 * Puts an arrow into a loop, while allowing the UI to remain responsive. The arrow should return either
 * Repeat(x) or Done(x), to repeat or exit the loop respectively.
 *
 * Signals progress when the loop completes.
 */

function Repeat(x) { return { Repeat:true, value:x }; }
function Done(x)   { return { Done:true,   value:x }; }

AsyncA.RepeatThunk = function(f) {
    this.f = f;
}
AsyncA.RepeatThunk.prototype = new AsyncA(function(x, a) {
    a.cont(x, this.f, new AsyncA.RepeatThunk.InnerThunk(this.f, a));
});
AsyncA.RepeatThunk.prototype.makeName = function() {
    return "repeat " + this.f.makeName();
}
AsyncA.RepeatThunk.InnerThunk = function(f, a) {
    this.f = f;
    this.cancelled = false;

    var self = this;
    this.canceller = function() {
        self.cancelled = true;
    };
    a.addCanceller(this.canceller);
}
AsyncA.RepeatThunk.InnerThunk.prototype = new AsyncA(function(x, a) {
    if (this.cancelled) {
        return;
    }
    if (x.Repeat) {
        a.cont(x.value, this.f, this);
    } else if (x.Done) {
        a.advance(this.canceller);
        a.cont(x.value);
    } else {
        throw new TypeError("Repeat or Done?");
    }
});
AsyncA.RepeatThunk.InnerThunk.prototype.makeName = function() {
    return "repeatinner " + this.f.makeName();
}

AsyncA.prototype.repeat = function() {
    return new AsyncA.RepeatThunk(this);
}
Function.prototype.repeat = function() {
    return this.AsyncA().repeat();
}


/*
 * AsyncA.prototype.animate(): animateing operator.
 *
 * Like repeat, puts an arrow into a loop, but yields to the UI thread at every iteration. This is useful for animation
 * as it limits the loop to the event update rate (typically 100Hz). The arrow should return either Repeat(x) or
 * Done(x), to repeat or exit the loop respectively.
 *
 * Signals progress beanimate every iteration.
 *
 * Note: don't use animate if a momentary delay is undesirable, such as when reinstalling an EventA arrow, since this
 * may result in a momentary (visible) loss in event tracking (e.g., beanimate mousemove events with the mouse button down
 * (dragging), the delay causes text to be momentarily selected).
 */

AsyncA.AnimateThunk = function(f, interval) {
    this.f = f;
    this.interval = interval || 0;
}
AsyncA.AnimateThunk.prototype = new AsyncA(function(x, a) {
    a.cont(Repeat(x), new AsyncA.AnimateThunk.InnerThunk(this.f, this.interval));
});
AsyncA.AnimateThunk.prototype.makeName = function() {
    return "animate " + this.f.makeName();
}
AsyncA.AnimateThunk.InnerThunk = function(f, interval) {
    this.f = f;
    this.interval = interval;
}
AsyncA.AnimateThunk.InnerThunk.prototype = new AsyncA(function(x, a) {
    if (x.Repeat) {
        var self = this;
        var timerid = setTimeout(function() {
            a.advance(self.canceller);
            a.cont(x.value, self.f, self);
        }, this.interval);
        this.canceller = function() {
            clearTimeout(timerid);
        }
        a.addCanceller(this.canceller);
    } else if (x.Done) {
        a.advance(this.canceller);
        a.cont(x.value);
    } else {
        throw new TypeError("Repeat or Done?");
    }
});
AsyncA.AnimateThunk.InnerThunk.prototype.makeName = function() {
    return "animateinner " + this.f.makeName();
}

AsyncA.prototype.animate = function(interval) {
    return new AsyncA.AnimateThunk(this, interval);
}
Function.prototype.animate = function(interval) {
    return this.AsyncA().animate(interval);
}



/*
 * AsyncA.prototype.or(): either-or combinator.
 *
 * Given two AsyncA arrows, create a composite arrow that allow only one of the components, whichever is the first to
 * trigger, to execute. The other arrow will be cancelled.
 *
 */
AsyncA.OrThunk = function(f, g) {
    this.f = f;
    this.g = g;
}
AsyncA.OrThunk.prototype = new AsyncA(function(x, a) {
    var p1, p2;
    function cancel() {
        if (p1) p1.cancel();
        if (p2) p2.cancel();
    }

    /* and run both */
    a.addCanceller(cancel);
    p1 = this.f.next(function(y) {
        if (p2) {
            p2.cancel();
        }
        p1 = null;
        a.advance(cancel);
        a.cont(y);
    }).run(x, a);
    p1.next(function() {
        p2.cancel();
        p2 = null;
    }).run();

    if (p1) {
        p2 = this.g.next(function(y) {
            if (p1) {
                p1.cancel();
            }
            a.advance(cancel);
            a.cont(y);
        }).run(x, a);
        p2.next(function() {
            p1.cancel();
            p1 = null;
        }).run();
    }
});

AsyncA.prototype.or = function(g) {
    return new AsyncA.OrThunk(this, g.AsyncA());
}
Function.prototype.or = function(g) {
    return this.AsyncA().or(g);
}




function DelayA(delay) {
    if (!(this instanceof DelayA))
        return new DelayA(delay);
    this.delay = delay;
}
DelayA.prototype = new AsyncA(function(data, a) {
    var self = this;
    var timerid = setTimeout(function() {
        a.advance(canceller);
        a.cont(data);
    }, this.delay);
    function canceller() {
        clearTimeout(timerid);
    }
    a.addCanceller(canceller);
});




/*
 * EventA: Arrows for event handling on HTML elements, constructed on AsyncA, with support for progress and
 * cancellation.
 *
 * When run, EventA installs an event handler on the input and waits for the event. When it fires, it then uninstalls
 * the event handler and passes the event object to the next arrow.
 */

function EventA(eventname) {
    if (!(this instanceof EventA))
        return new EventA(eventname);
    this.eventname = eventname;
}
EventA.prototype = new AsyncA(function(target, a) {
    var f = this;
    function cancel() {
        target.removeEventListener(f.eventname, handler, false);
    }
    function handler(event) {
        cancel();
        a.advance(cancel);
        a.cont(event);
    }
    a.addCanceller(cancel);
    target.addEventListener(f.eventname, handler, false);
});



function HttpA(defaults) {
    if (!(this instanceof HttpA))
        return new HttpA(defaults);
    this.defaults = defaults;
}
HttpA.prototype = new AsyncA(function(param, a) {
    param = param || this.defaults;
    for (k in this.defaults) {
        if (!(k in param)) {
            param[k] = this.defaults[k];
        }
    }
    var request = new XMLHttpRequest();
    var url = param.url;
    if (!(typeof url == "string")) {
        throw new Error("No URL to load");
    }

    function cancel() {
        request.abort();
    }
    a.addCanceller(cancel);

    request.open("GET", url, true);
    request.onreadystatechange = function() {
        if (request.readyState == 4) {
            a.advance(cancel);
            a.cont(request);
        } else {
            /* TODO: send something to progress arrow */
        }
    };
    request.send(null);
});


var ConstA = function(x) {
    return function() { return x; }.AsyncA();
}


var ElementA = function(el) {
    if (typeof el === "string" || el instanceof String) {
        return function() {
            return document.getElementById(el);
        }.AsyncA();
    // } else if (el instanceof Element || el instanceof Document || el instanceof DOMWindow) {
    } else if (el.nodeType == 1 || el.nodeType == 9 || el === window) { /* IE compatibility */
        return ConstA(el);
    } else {
        throw new TypeError("Not a DOM element/document/window!");
    }
}

