// Type representations:
//
// number are numbers
//
// cons pairs are [first, rest]
// 
// function closures are Closures
// primitive procedures are regular functions.


// No error trapping at the moment.


var Frame = function(label, proc) {
    this.label = label;
    this.proc = proc;
};



// A closure consists of its free variables as well as a label
// into its text segment.
var Closure = function(label, arity, closedVals, displayName) {
    this.label = label;
    this.arity = arity;
    this.closedVals = closedVals;
    this.displayName = displayName;
};

// A primitive function is just a Javascript function.



var Primitives = (function() {
    var NULL = [];
    return {
	'display': function(MACHINE, arity) {
	    var firstArg = MACHINE.env[MACHINE.env.length-1];
            MACHINE.params.currentDisplayer(firstArg);
	},

	'newline': function(MACHINE, arity) {
            MACHINE.params.currentDisplayer("\n");
	},

	'displayln': function(MACHINE, arity){
	    var firstArg = MACHINE.env[MACHINE.env.length-1];
            MACHINE.params.currentDisplayer(firstArg);
            MACHINE.params.currentDisplayer("\n");
	},

	'pi' : Math.PI,

	'e' : Math.E,

	'=': function(MACHINE, arity) {
	    var firstArg = MACHINE.env[MACHINE.env.length-1];
	    var secondArg = MACHINE.env[MACHINE.env.length-2];
            return firstArg === secondArg;
	},

	'<': function(MACHINE, arity) {
	    var firstArg = MACHINE.env[MACHINE.env.length-1];
	    var secondArg = MACHINE.env[MACHINE.env.length-2];
	    return firstArg < secondArg;
	},

	'>': function(MACHINE, arity) {
	    var firstArg = MACHINE.env[MACHINE.env.length-1];
	    var secondArg = MACHINE.env[MACHINE.env.length-2];
	    return firstArg > secondArg;
	},

	'<=': function(MACHINE, arity) {
	    var firstArg = MACHINE.env[MACHINE.env.length-1];
	    var secondArg = MACHINE.env[MACHINE.env.length-2];
	    return firstArg <= secondArg;
	},

	'>=': function(MACHINE, arity) {
	    var firstArg = MACHINE.env[MACHINE.env.length-1];
	    var secondArg = MACHINE.env[MACHINE.env.length-2];
	    return firstArg >= secondArg;
	},
	
	'+': function(MACHINE, arity) {
	    var firstArg = MACHINE.env[MACHINE.env.length-1];
	    var secondArg = MACHINE.env[MACHINE.env.length-2];

            return firstArg + secondArg;
	},
	
	'*': function(MACHINE, arity) {
	    var firstArg = MACHINE.env[MACHINE.env.length-1];
	    var secondArg = MACHINE.env[MACHINE.env.length-2];
            return firstArg * secondArg;
	},
	
	'-': function(MACHINE, arity) {
	    var firstArg = MACHINE.env[MACHINE.env.length-1];
	    var secondArg = MACHINE.env[MACHINE.env.length-2];
            return firstArg - secondArg;
	},
	
	'/': function(MACHINE, arity) {
	    var firstArg = MACHINE.env[MACHINE.env.length-1];
	    var secondArg = MACHINE.env[MACHINE.env.length-2];
	    return firstArg / secondArg;
	},

	'cons': function(MACHINE, arity) {
	    var firstArg = MACHINE.env[MACHINE.env.length-1];
	    var secondArg = MACHINE.env[MACHINE.env.length-2];
	    return [firstArg, secondArg];
	},

	'list': function(MACHINE, arity) {
	    var result = NULL;
	    for (var i = 0; i < arity; i++) {
		result = [MACHINE.env[MACHINE.env.length - (arity - i)],
			  result];
	    }
	    return result;
	},

	'car': function(MACHINE, arity) {
	    var firstArg = MACHINE.env[MACHINE.env.length-1];
	    return firstArg[0];
	},

	'cdr': function(MACHINE, arity) {
	    var firstArg = MACHINE.env[MACHINE.env.length-1];
	    return firstArg[1];
	},

	'pair?': function(MACHINE, arity) {
	    var firstArg = MACHINE.env[MACHINE.env.length-1];
	    return (typeof(firstArg) == 'object' && 
		    firstArg.length === 2);
	},

	'set-car!': function(MACHINE, arity) {
	    var firstArg = MACHINE.env[MACHINE.env.length-1];
	    var secondArg = MACHINE.env[MACHINE.env.length-2];
	    firstArg[0] = secondArg;
	},

	'set-cdr!': function(MACHINE, arity) {
	    var firstArg = MACHINE.env[MACHINE.env.length-1];
	    var secondArg = MACHINE.env[MACHINE.env.length-2];
	    firstArg[1] = secondArg;
	},
	
	'not': function(MACHINE, arity) {
	    var firstArg = MACHINE.env[MACHINE.env.length-1];
	    return (!firstArg);
	},

	'null' : NULL,

	'null?': function(MACHINE, arity) {
	    var firstArg = MACHINE.env[MACHINE.env.length-1];
	    return firstArg === NULL;
	},

	'add1': function(MACHINE, arity) {
	    var firstArg = MACHINE.env[MACHINE.env.length-1];
	    return firstArg + 1;
	},

	'sub1': function(MACHINE, arity) {
	    var firstArg = MACHINE.env[MACHINE.env.length-1];
	    return firstArg - 1;
	},

	'zero?': function(MACHINE, arity) {
	    var firstArg = MACHINE.env[MACHINE.env.length-1];
	    return firstArg === 0;
	},

	'vector': function(MACHINE, arity) {
	    var i;
	    var result = [];
	    for (i = 0; i < arity; i++) {
		result.push(MACHINE.env[MACHINE.env.length-1-i]);
	    }
	    return result;
	},

	'vector->list': function(MACHINE, arity) {
	    var firstArg = MACHINE.env[MACHINE.env.length-1];
	    var i;
	    var result = NULL;
	    for (i = 0; i < firstArg.length; i++) {
		result = [firstArg[firstArg.length - 1 - i], result];
	    }
	    return result;
	},
	
	'list->vector': function(MACHINE, arity) {
	    var firstArg = MACHINE.env[MACHINE.env.length-1];
	    var result = [];
	    while (firstArg !== NULL) {
		result.push(firstArg[0]);
		firstArg = firstArg[1];
	    }
	    return result;
	},

	'vector-ref': function(MACHINE, arity) {
	    var firstArg = MACHINE.env[MACHINE.env.length-1];
	    var secondArg = MACHINE.env[MACHINE.env.length-2];
	    return firstArg[secondArg];
	},

	'vector-set!': function(MACHINE, arity) {
	    var firstArg = MACHINE.env[MACHINE.env.length-1];
	    var secondArg = MACHINE.env[MACHINE.env.length-2];
	    var thirdArg = MACHINE.env[MACHINE.env.length-3];
	    firstArg[secondArg] = thirdArg;
	    return null;
	},

	'symbol?': function(MACHINE, arity) {
	    var firstArg = MACHINE.env[MACHINE.env.length-1];
	    return typeof(firstArg) === 'string';
	},

	'symbol->string': function(MACHINE, arity) {
	    var firstArg = MACHINE.env[MACHINE.env.length-1];
	    return firstArg;
	},

	'string-append': function(MACHINE, arity) {
	    var buffer = [];
	    var i;
	    for (i = 0; i < arity; i++) {
		buffer.push(MACHINE.env[MACHINE.env.length - 1 - i]);
	    }
	    return buffer.join('');
	},

	'string-length': function(MACHINE, arity) {
	    var firstArg = MACHINE.env[MACHINE.env.length-1];
	    return firstArg.length;
	},
	
	'box': function(MACHINE, arity) {
	    var firstArg = MACHINE.env[MACHINE.env.length-1];
	    var result = [firstArg];
	    return result;
	},

	'unbox': function(MACHINE, arity) {
	    var firstArg = MACHINE.env[MACHINE.env.length-1];
	    return firstArg[0];
        },

	'set-box!': function(MACHINE, arity) {
	    var firstArg = MACHINE.env[MACHINE.env.length-1];
	    var secondArg = MACHINE.env[MACHINE.env.length-2];
	    firstArg[0] = secondArg;
	    return;
	},

	'void': function(MACHINE, arity) {
	    return;
	},


	'eq?': function(MACHINE, arity) {
	    var firstArg = MACHINE.env[MACHINE.env.length-1];
	    var secondArg = MACHINE.env[MACHINE.env.length-2];
	    return firstArg === secondArg;
	},

	'equal?': function(MACHINE, arity) {
	    var firstArg = MACHINE.env[MACHINE.env.length-1];
	    var secondArg = MACHINE.env[MACHINE.env.length-2];
	    var lset = [firstArg], rset = [secondArg];
	    while (lset.length !== 0 && rset.length !== 0) {
		var lhs = lset.pop();
		var rhs = rset.pop();
		if (lhs === rhs) {
		    continue;
		} else if (typeof(lhs) === 'object' &&
			   typeof(rhs) === 'object' &&
			   typeof(lhs.length) === 'number' &&
			   typeof(rhs.length) === 'number' &&
			   lhs.length === rhs.length) {
		    lset.push.apply(lset, lhs);
		    rset.push.apply(rset, rhs);
		} else {
		    return false;
		}
	    }
	    return true;
	}
    };
})();



var MACHINE = { callsBeforeTrampoline: 100, 
		val:undefined,
		proc:undefined, 
		env: [],
		control : [],
		running : false,
		params: { currentDisplayer: function(v) {},

			  currentSuccessHandler: function(MACHINE) {},
			  currentErrorHandler: function(MACHINE, exn) {},

			  currentNamespace: {},

			  // These parameters control how often
			  // control yields back to the browser
			  // for response.  The implementation is a
			  // simple PID controller.
			  //
			  // To tune this, adjust desiredYieldsPerSecond.
			  // Do no touch numBouncesBeforeYield or
			  // maxNumBouncesBeforeYield, because those
			  // are adjusted automatically by the
			  // recomputeMaxNumBouncesBeforeYield
			  // procedure.
			  desiredYieldsPerSecond: 5,
			  numBouncesBeforeYield: 2000,   // self-adjusting
			  maxNumBouncesBeforeYield: 2000 // self-adjusting
			}
	      };



// recomputeGas: state number -> number
var recomputeMaxNumBouncesBeforeYield = function(observedDelay) {
    // We'd like to see a delay of DESIRED_DELAY_BETWEEN_BOUNCES so
    // that we get MACHINE.params.desiredYieldsPerSecond bounces per
    // second.
    var DESIRED_DELAY_BETWEEN_BOUNCES = 
	(1000 / MACHINE.params.desiredYieldsPerSecond);
    var ALPHA = 256;
    var delta = (ALPHA * ((DESIRED_DELAY_BETWEEN_BOUNCES -
			   observedDelay) / 
			  DESIRED_DELAY_BETWEEN_BOUNCES));
    MACHINE.params.maxNumBouncesBeforeYield = 
        Math.max(MACHINE.params.maxNumBouncesBeforeYield + delta,
                 1);
};


var trampoline = function(MACHINE, initialJump) {
    var thunk = initialJump;
    var startTime = (new Date()).valueOf();
    MACHINE.callsBeforeTrampoline = 100;
    MACHINE.params.numBouncesBeforeYield = 
	MACHINE.params.maxNumBouncesBeforeYield;
    MACHINE.running = true;

    while(thunk) {
        try {
            thunk(MACHINE);
	    break;
        } catch (e) {
            if (typeof(e) === 'function') {
                thunk = e;
                MACHINE.callsBeforeTrampoline = 100;

		if (MACHINE.params.numBouncesBeforeYield-- < 0) {
		    recomputeMaxNumBouncesBeforeYield(
			(new Date()).valueOf() - startTime);
		    setTimeout(
			function() {
			    trampoline(MACHINE, thunk);
			},
			0);
		    return;
		}
            } else {
		MACHINE.running = false;
	        return MACHINE.params.currentErrorHandler(MACHINE, e);
            }
        }
    }
    MACHINE.running = false;
    return MACHINE.params.currentSuccessHandler(MACHINE);
};