/* Copyright (c) 2008 Khoo Yit Phang (http://www.cs.umd.edu/projects/PL/arrowlets)
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

/*
 * 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() {
    return "[AsyncA " + this.toAString() + "]";
}
AsyncA.prototype.toAString = 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 implementation of ProgressA is actually split across ProgressA as the public interface, and AsyncA.Instance
 * containing the private interface.
 */

function ProgressA(target) {
    if (!(this instanceof ProgressA))
        return new ProgressA(target);
    this.target = target;
    this.eventlisteners = {};
}
ProgressA.prototype = new AsyncA(function(x, a) {
    a.cont(this);
});
ProgressA.prototype.toAString = function() {
    return "ProgressA";
}
ProgressA.prototype.cancel = function() {
    this.target.cancel();
}
/* DOM EventTarget interface: http://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-EventTarget */
ProgressA.prototype.addEventListener = function(eventname, handler, capturing) {
    if (!(eventname in this.eventlisteners)) {
        this.eventlisteners[eventname] = [[], []]; /* initialize when eventname is first seen */
    }
    var listeners = this.eventlisteners[eventname][capturing ? 1 : 0];
    var index = listeners.indexOf(handler);
    if (index < 0) { /* doesn't exist */
        listeners.push(handler);
    }
}
ProgressA.prototype.removeEventListener = function(eventname, handler, capturing) {
    if (!(eventname in this.eventlisteners)) {
        return;
    }
    var listeners = this.eventlisteners[eventname][capturing ? 1 : 0];
    var index = listeners.indexOf(handler);
    if (index >= 0) {
        listeners.splice(index, 1); /* found, remove */
    }
}
ProgressA.prototype.dispatchEvent = function(event) {
    /* TODO: should preventDefault() or stopPropogation() affect ProgressA? */
    var eventname = event.type;
    if (!(eventname in this.eventlisteners)) {
        return;
    }
    for (var capturing = 1; capturing >= 0; capturing--) {
        var listeners = this.eventlisteners[eventname][capturing].concat(); /* clone */
        var length = listeners.length;
        for (var i = 0; i < length; i++) {
            listeners[i](event);
        }
    }
}

/* ProgressA.Event should be like DOM Event interface, but it doesn't quite fit:
 * http://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-Event */
ProgressA.Event = function(eventname, detail) {
    this.type = eventname;
    this.detail = detail;
}

/* ProgressA private interface */
AsyncA.Instance.prototype.signal = function(event, detail) {
    if (typeof event === "string" || event instanceof String) {
        event = new ProgressA.Event(event, detail);
    }
    this.progressA.dispatchEvent(event, this.progressA);
}
AsyncA.Instance.prototype.addCanceller = function(canceller) {
    this.cancellers.push(canceller);
}
AsyncA.Instance.prototype.advance = function(canceller) {
    /* remove canceller function */
    var index = this.cancellers.indexOf(canceller);
    if (index >= 0) {
        this.cancellers.splice(index, 1);
    }
    /* signal progress */
    this.signal("progress");
}
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.toAString = function() {
    /* pull out the function name if available */
    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.toAString = function() {
    /* TODO: this darn thing can cause a stack overflow */
    return "(" + this.f.toAString() + ">>>" + this.g.toAString() + ")";
}
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.toAString = function() {
    return "repeat " + this.f.toAString();
}
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.toAString = function() {
    return "repeatinner " + this.f.toAString();
}

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


/*
 * AsyncA.prototype.animate(): animating 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 at 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., during 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.toAString = function() {
    return "animate " + this.f.toAString();
}
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.toAString = function() {
    return "animateinner " + this.f.toAString();
}

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(trigger, f, g) {
    this.f = f;
    this.g = g;
    /* allow trigger to be blank string "" (undefined == null) */
    this.trigger = (trigger == null ? "progress" : trigger);
}
AsyncA.OrThunk.prototype = new AsyncA(function(x, a) {
    var p1, p2;
    function cancel() {
        if (p1) p1.cancel();
        if (p2) p2.cancel();
    }

    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(EventA(this.trigger)).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(EventA(this.trigger)).next(function() {
            p1.cancel();
            p1 = null;
        }).run();
    }
});
AsyncA.OrThunk.prototype.toAString = function() {
    return "(" + this.f.toAString() + " or'" + this.trigger + " " + this.g.toAString() + ")";
}

AsyncA.prototype.or = function(g, h) {
    if (h === undefined) {
        return new AsyncA.OrThunk(null, this, g.AsyncA());
    } else {
        return new AsyncA.OrThunk(g, this, h.AsyncA());
    }
}
Function.prototype.or = function(g, h) {
    return this.AsyncA().or(g, h);
}




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);
});
DelayA.prototype.toAString = function() {
    return "DelayA(" + this.delay + ")";
}




/*
 * 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);
});
EventA.prototype.toAString = function() {
    return "EventA(" + this.eventname + ")";
}



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);
});
HttpA.prototype.toAString = function() {
    return "HttpA";
}


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


function ElementA(el) {
    if (typeof el === "string" || el instanceof String) {
        return function ElementA() {
            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 function ElementA() { return el; }.AsyncA();
    } else {
        throw new TypeError("Not a DOM element/document/window!");
    }
}


function SignalA(eventname) {
    if (!(this instanceof SignalA))
        return new SignalA(eventname);
    this.eventname = (eventname == null ? "signal" : eventname);
}
SignalA.prototype = new AsyncA(function(x, a) {
    a.signal(this.eventname, x);
    a.cont(x);
});
SignalA.prototype.toAString = function() {
    return "SignalA(" + this.eventname + ")";
}
