// Jitter, a jQuery-powered presentation library.

String.prototype.resolve = function (vars) {
    function repl(s, v) { return vars[v]; }
    return this.replace(/\{([^}]+)\}/g, repl);
};

function set(a) {
    // Convert the given array into a set.
    var o = {};
    for (var i=0; i < a.length; i++) o[a[i]] = '';
    return o;
}

function merge (obj1, obj2) {
    var result = {};
    for (var k in obj1) result[k] = obj1[k];
    for (var k in obj2) result[k] = obj2[k];
    return result;
};

function items(obj) {
    // Return a list of (key, value) pairs for the given object
    var result = [];
    if (typeof(obj) == 'object') {
        for (var key in obj) result[result.length] = [key, obj[key]];
    } else {
        for (var i=0; i < obj.length; i++) result.push(obj[i]);
    }
    return result;
};

// Some jQuery plugins:

jQuery.fn.vcenter = function(cssFloat) {
    return this.each(function() {
        var j = $(this);
        if (cssFloat) this.style.cssFloat = cssFloat;
        var h = j.height();
        var ph = j.parent().height();
        var mt = ((ph / 2) - (h / 2));
        this.style.marginTop = mt.toString() + 'px';
    });
};

jQuery.fn.center = function() {
    // Returns position() plus half height/width.
    var results;
    if (this[0]) {
        results = this.position();
        var mrg = this.marginPos();
        results.top = results.top + mrg.top + (this.height() / 2);
        results.left = results.left + mrg.left + (this.width() / 2);
    }
    return results;
};

jQuery.fn.stackpos = function(target) {
    // Return a position()-like {top, left} object for stacking this object
    // on the given target.
    var results;
    if (this[0] && target[0]) {
        results = target.center();
        results.top -= (this.height() / 2);
        results.left -= (this.width() / 2);
    }
    return results;
};

jQuery.fn.stack = function(target) {
    // Stack the (absolutely-positioned) self on the given target
    return this.each(function() {
        var elem = $(this);
        elem.css(elem.stackpos(target));
    });
};

jQuery.fn.marginPos = function(recurse) {
    // Return a position()-like {top, left} object for marginTop/Left.
    var results;
    if (this[0]) {
        var mt = 0, ml = 0;
        if (this[0].style) {
            mt = this[0].style.marginTop.match(/([0-9.-]+)px/);
            mt = mt ? parseInt(mt[1]) : 0;
            ml = this[0].style.marginLeft.match(/([0-9.-]+)px/);
            ml = ml ? parseInt(ml[1]) : 0;
        }
        
        if (recurse) {
            if (this[0].parentNode != null) {
                var pmrg = $(this[0].parentNode).marginPos(recurse);
                mt = mt + pmrg.top;
                ml = ml + pmrg.left;
            }
        }
        
        results = {top: mt, left: ml};
    }
    return results;
};

jQuery.fn.detach = function(rehome) {
    // Make all matching elements into position:absolute ones.
    // Keep them in the same window position.
    // If 'rehome' is true, unhook the element from its current
    // container and re-attach it to the body element.
    return this.each(function() {
        var j = $(this);
        // var pos = j.position();
        // var mrg = j.marginPos(rehome);
        var rect = j.offset();
        j.css({position: 'absolute', cssFloat: 'none',
               top: rect.top, left: rect.left,
               width: j.width(), height: j.height(),
               marginTop: 0, marginLeft: 0
               });
        if (rehome) j.remove().appendTo("body");
    });
};

jQuery.fn.fadeAway = function(speed, callback) {
    var self = this;
    return self.fadeOut(speed, function () { self.remove(); if (callback) callback(); });
};

jQuery.fn.grow = function(factor, speed) {
    return this.each(function() {
        var j = $(this);
        j.animate({width: (j.width() * factor), height: (j.height() * factor)}, speed);
    });
};


// The Jitter namespace itself.

var Jitter = {};


// ----------------------- The Jitter.Script class ----------------------- //

Jitter.Script = function () {
    this.scenes = [];
    this.scene_pointer = -1;
    // The cues.indexOf the most recently executed cue. Range: -1 to cues.length.
    // That is, if cue_pointer is 1, then advance() will execute cues[2]
    // and retreat() will execute cues[1].undo.
    this.cue_pointer = -1;
};

Jitter.Script.prototype.advance = function () {
    if (this.scenes.length) {
        if (this.scene_pointer >= this.scenes.length) return;
        if (this.scene_pointer < 0) {
            this.scene_pointer = 0;
            this.cue_pointer = -1;
            this.scenes[this.scene_pointer].begin();
        }
        var scene = this.scenes[this.scene_pointer];
        
        if (this.cue_pointer < 0) this.cue_pointer = -1; // just in case
        
        while (this.cue_pointer >= (scene.cues.length - 1)) {
            // End the current scene and begin the next
            scene.end()
            this.scene_pointer++;
            this.cue_pointer = -1;
            if (this.scene_pointer < this.scenes.length) {
                scene = this.scenes[this.scene_pointer];
                scene.begin();
            } else {
                return;
            }
        }
        
        // Run the current cue
        this.cue_pointer++;
        scene.cues[this.cue_pointer]();
    }
};

Jitter.Script.prototype.retreat = function () {
    if (this.scenes.length) {
        if (this.scene_pointer < 0) return;
        if (this.scene_pointer >= this.scenes.length) {
            this.scene_pointer = this.scenes.length - 1;
            var scene = this.scenes[this.scene_pointer];
            this.cue_pointer = scene.cues.length - 1;
            scene.unend();
        } else {
            var scene = this.scenes[this.scene_pointer];
        }
        
        if (this.cue_pointer >= scene.cues.length)
            this.cue_pointer = scene.cues.length - 1; // just in case
        
        // Run the current cue.undo
        if (scene.cues.length && this.cue_pointer >= 0) {
            var cue = scene.cues[this.cue_pointer];
            if (cue.undo != null) cue.undo();
            this.cue_pointer--;
        }
        
        while (this.cue_pointer < 0) {
            // Unbegin the current scene and move to the previous
            scene.unbegin();
            this.scene_pointer--;
            if (this.scene_pointer >= 0) {
                scene = this.scenes[this.scene_pointer];
                this.cue_pointer = scene.cues.length - 1;
                scene.unend();
            } else {
                this.cue_pointer = -1;
                return;
            }
        }
    }
};

Jitter.Script.prototype.jump_forward = function () {
    if (this.scenes) {
        if (this.scene_pointer < 0) {
            this.scene_pointer = 0;
        } else if (this.scene_pointer >= this.scenes.length) {
            this.scene_pointer = this.scenes.length; // Just in case
        } else {
            this.scenes[this.scene_pointer].end();
            this.scene_pointer++;
            this.cue_pointer = -1;
            if (this.scene_pointer < this.scenes.length) {
                scene = this.scenes[this.scene_pointer];
                scene.begin();
            }
        }
    }
};

Jitter.Script.prototype.jump_backward = function () {
    if (this.scenes) {
        if (this.scene_pointer < 0) {
            this.scene_pointer = -1;
        } else if (this.scene_pointer >= this.scenes.length) {
            this.scene_pointer = this.scenes.length - 1;
        } else {
            if (this.cue_pointer == -1) {
                // At the first cue. Jump up one scene.
                this.scenes[this.scene_pointer].unbegin();
                this.scene_pointer -= 1;
                if (this.scene_pointer >= 0) {
                    this.scenes[this.scene_pointer].unend();
                    this.scenes[this.scene_pointer].unbegin();
                }
            } else {
                // Jump before the first cue in this scene.
                this.scenes[this.scene_pointer].unbegin();
            }
            this.cue_pointer = -1;
        }
    }
};

Jitter.Script.prototype.present = function () {
    // Bind mouse and key events to run this script.
    var self = this;
    $(document).click(function () { self.advance(); });
    $(document).keypress(function (e) {
        if (!e) var e = window.event;
        if (e.keyCode) { var code = e.keyCode; }
        else { if (e.which) var code = e.which; }
        if (code == 37) return self.retreat();
        if (code == 38 || code == 33) return self.jump_backward();
        if (code == 39) return self.advance();
        if (code == 40 || code == 34) return self.jump_forward();
    });
};

Jitter.Script.prototype.addScene = function () {
    var scene = new Jitter.Scene();
    this.scenes.push(scene);
    return scene;
};


// ----------------------- The Jitter.Scene class ----------------------- //

Jitter.Scene = function () {
    this.cues = [];
};

Jitter.Scene.prototype.begin = function () {
    $(".jitter-prop").remove();
};

Jitter.Scene.prototype.unbegin = function () {
    $(".jitter-prop").remove();
};

Jitter.Scene.prototype.end = function () {
    $(".jitter-prop").remove();
};

Jitter.Scene.prototype.unend = function () {
    $(".jitter-prop").remove();
};

Jitter.Scene.prototype.addStyle = function (style) {
    this.add(function () { style.create(); },
             function () { style.remove(); });
};

Jitter.Scene.prototype.assertStyle = function (style) {
    this.add(function () { style.assert(); },
             function () { style.remove(); });
};

Jitter.Scene.prototype.removeStyle = function (style) {
    this.add(function () { style.remove(); },
             function () { style.create(); });
};

Jitter.Scene.prototype.add = function (func, undo) {
    func.undo = undo;
    this.cues.push(func);
};

Jitter.Scene.prototype.fadeIn = function (prop, speed) {
    // Add a cue to fadeIn the given property (and an undo which fades it out).
    this.add(function () { prop.$().fadeIn(speed); },
             function () { prop.$().fadeOut(speed); });
};

Jitter.Scene.prototype.fadeReplace = function (target, replacement, speed, callback) {
    // Add a cue which fades out the target Prop and fades the replacement
    // Prop into its existing position.
    this.add(
        function () {
            replacement.placeAfter("#" + target.id);
            target.detach(true).fadeAway(speed);
            replacement.fadeIn(speed, callback);
        },
        function () {
            target.placeBefore("#" + replacement.id);
            replacement.detach(true).fadeAway(speed);
            target.fadeIn(speed, callback);
        });
};

Jitter.Scene.prototype.fadeAppend = function (prop, target, speed, callback) {
    // Add a cue to append the Prop to the target (selector) and fade it in.
    this.add(function () { prop.appendTo(target).fadeIn(speed, callback); },
             function () { prop.fadeAway(speed, callback); });
};

Jitter.Scene.prototype.fadePrepend = function (prop, target, speed, callback) {
    // Add a cue to prepend the Prop to the target (selector) and fade it in.
    this.add(function () { prop.prependTo(target).fadeIn(speed, callback); },
             function () { prop.fadeAway(speed, callback); });
};

Jitter.Scene.prototype.fadeBefore = function (prop, target, speed, callback) {
    // Add a cue to place the Prop before the target prop and fade it in.
    this.add(function () { prop.placeBefore("#" + target.id).fadeIn(speed, callback); },
             function () { prop.fadeAway(speed, callback); });
};

Jitter.Scene.prototype.fadeAfter = function (prop, target, speed, callback) {
    // Add a cue to place the Prop after the target prop and fade it in.
    this.add(function () { prop.placeAfter("#" + target.id).fadeIn(speed, callback); },
             function () { prop.fadeAway(speed, callback); });
};

Jitter.Scene.prototype.fadeAway = function (prop, target, speed, callback) {
    // Add a cue to remove the Prop from the target (selector) and fade it out.
    this.add(function () { prop.fadeAway(speed, callback); },
             function () { prop.appendTo(target).fadeIn(speed, callback); });
};

// MarkedScene, a subclass of Scene that updates #scenemarker.html

Jitter.MarkedScene = function () {
    this.cues = [];
    Jitter.MarkedScene.scene_count += 1;
    this.scene_number = Jitter.MarkedScene.scene_count;
};
Jitter.MarkedScene.scene_count = 0;
Jitter.MarkedScene.prototype = new Jitter.Scene;

Jitter.MarkedScene.prototype.begin = function () {
    $(".jitter-prop").remove();
    $("#scene-marker").html(this.scene_number);
};

Jitter.MarkedScene.prototype.unend = function () {
    $(".jitter-prop").remove();
    $("#scene-marker").html(this.scene_number);
};


// -------------------- The Jitter.manager object --------------------- //

Jitter.manager = {
    props: [],
    // Number of times per second to run the manager.
    rate: 5,
    cycle: null,
    recycle: function (rate) {
        if (this.cycle) {
            clearInterval(this.cycle);
            this.cycle = null;
        }
        this.rate = rate;
        if (rate) this.cycle = setInterval("Jitter.manager.main()", (1 / rate));
    },
    main: function () {
        for (var i in this.props) {
            var prop = this.props[i];
            if (prop.tick) prop.tick();
        }
    }
};
Jitter.manager.recycle(5);

// ----------------------- The Jitter.Prop class ----------------------- //

Jitter.Prop = function (id, content, initialStyle, wrapper) {
    this.id = id;
    if (wrapper == null) wrapper = 'div';
    this.content = content;
    this.element = ("<{wrapper} id='{id}' class='jitter-prop' style='display: none'>{content}" +
                    "</{wrapper}>").resolve({id:id, content:content, wrapper:wrapper});
    this.initialStyle = initialStyle;
    Jitter.manager.props.push(this);
};

Jitter.Prop.prototype.$ = function () {
    return $("#" + this.id);
};

Jitter.Prop.prototype.toString = function () {
    return this.element;
};

Jitter.Prop.prototype.appendTo = function (target) {
    // Append this Prop to the given target.
    // The 'target' argument may be a selector string, a jQuery object,
    // or another Jitter.Prop.
    // If target is null, the Prop will be appended to document.body.
    if (target == null) target = "body";
    if (typeof(target) == 'string') target = $(target);
    target.append(this.element);
    if (this.initialStyle) this.css(this.initialStyle);
    return this;
};

Jitter.Prop.prototype.prependTo = function (target) {
    // Prepend this Prop to the given target.
    // The 'target' argument may be a selector string, a jQuery object,
    // or another Jitter.Prop.
    // If target is null, the Prop will be prepended to document.body.
    if (target == null) target = "body";
    if (typeof(target) == 'string') target = $(target);
    target.prepend(this.element);
    if (this.initialStyle) this.css(this.initialStyle);
    return this;
};

Jitter.Prop.prototype.placeBefore = function (target) {
    // Create this Prop and place it before the given target.
    // The 'target' argument may be a selector string, a jQuery object,
    // or another Jitter.Prop.
    if (typeof(target) == 'string') target = $(target);
    target.before(this.element);
    if (this.initialStyle) this.css(this.initialStyle);
    return this;
};

Jitter.Prop.prototype.placeAfter = function (target) {
    // Create this Prop and place it after the given target.
    // The 'target' argument may be a selector string, a jQuery object,
    // or another Jitter.Prop.
    if (typeof(target) == 'string') target = $(target);
    target.after(this.element);
    if (this.initialStyle) this.css(this.initialStyle);
    return this;
};

Jitter.Prop.prototype.final = function () {
    if (this.final_content) this.$().html(this.final_content);
    return this;
};

Jitter.Prop.prototype.fadeAppend = function(target, speed, callback) {
    return this.appendTo(target).fadeIn(speed, callback);
};


// Copy most jQuery object functions onto the Jitter.Prop class.
for (var key in set([
    "prepend", "append", "before", "after",
    "attr", "css", "text", "clone", "val", "html", "eq", "parent",
    "parents", "next", "prev", "nextAll", "prevAll", "siblings",
    "children", "contents", "removeAttr", "addClass", "removeClass",
    "toggleClass", "remove", "empty", "data", "removeData",
    "queue", "dequeue", "bind", "one", "unbind", "trigger",
    "triggerHandler", "toggle", "hover", "ready", "live", "die",
    "blur", "focus", "load", "resize", "scroll", "unload",
    "click", "dblclick", "mousedown", "mouseup", "mousemove",
    "mouseover", "mouseout", "mouseenter", "mouseleave", "change",
    "select", "submit", "keydown", "keypress", "keyup", "error",
    "show", "hide", "fadeTo", "animate", "stop",
    "slideDown", "slideUp", "slideToggle", "fadeIn", "fadeOut",
    "offset", "position", "offsetParent", "scrollLeft", "scrollTop",
    "innerHeight", "outerHeight", "height", "innerWidth", "outerWidth", "width",
    "vcenter", "center", "stackpos", "stack", "marginPos", "detach",
    "fadeAway", "grow"])) {
    Jitter.Prop.prototype[key] = function () {
        // Bind the methodname into the closure
        var methodname = key;
        return function () {
            if ('$' in this) {
                var j = this.$();
                var method = j[methodname];
                var result = method.apply(j, arguments);
                // If possible, return the Prop object instead of the $()
                // one so extension methods like final() work.
                if (result == j) result = this;
                return result;
            } else {
                alert([methodname, this, arguments].toString());
            }
        };
    }();
}


Jitter.Props = function (id, lines, initialStyle, wrapper) {
    // Return an array of (numbered) Prop objects from the given array of content.
    var result = [];
    for (var i in lines) result[i] = new Jitter.Prop(id + i, lines[i], initialStyle, wrapper);
    return result;
};

Jitter.vcenter = function (cssFloat) {
    // Return a Prop.tick function which keeps the object vertically centered.
    return function () { return this.vcenter(cssFloat); };
}

// ----------------------- The Jitter.Style class ----------------------- //

Jitter.Style = function (id, rules) {
  this.id = id;
  this.rules = rules;
  this.element = document.createElement('style');
  this.element.id = this.id;
  $(this.element).attr({type: 'text/css', media: 'all'});
  this.created = false;
};

Jitter.Style.prototype.create = function () {
  $(this.element).appendTo('head');
  if (this.element.styleSheet) {
    // IE
    for (var i in this.rules)
      this.element.styleSheet.addRule(this.rules[i][0], this.rules[i][1]);
  } else if (this.element.sheet){
    // Firefox
    for (var i in this.rules) {
      var rule = this.rules[i][0] + ' {' + this.rules[i][1] + '}';
      this.element.sheet.insertRule(rule, i);
    }
  }
  this.created = true;
};

Jitter.Style.prototype.assert = function () {
  if (!this.created) this.create();
};

Jitter.Style.prototype.remove = function () {
  $(this.element).remove();
  this.created = false;
};

