document = {}
window = {}
navigator = {}
load("prototype-1.4.0.js");

load("example.js");
load("json.js");
load("jdbc.js");
load("inspect.js");

function callCC(f) {
    var k = new Continuation();
    return f(k);
}

Savannah = {}

Savannah.Servlet = Class.create();
Savannah.Servlet.prototype = {
    initialize: function () {
	this.entryPoints = [];
	this.unwinding = false;
	this.prng = java.security.SecureRandom.getInstance("SHA1PRNG");
	this.sha = java.security.MessageDigest.getInstance("SHA-1");
	this._current = new java.lang.ThreadLocal();
    },

    current: function () {
	return this._current.get();
    },

    session: function () {
	return this.current().sessionData;
    },

    continuationMap: function () {
	var sess = this.session();
	if (!sess.continuations) { sess.continuations = {}; }
	return sess.continuations;
    },

    addEntryPoint: function (spec, proc) {
	var parts = spec.split('/');
	var r = this.findEntryPoint(spec);
	if (r) {
	    r.entryPoint[0] = parts;
	    r.entryPoint[1] = proc;
	} else {
	    this.entryPoints.push([parts, proc]);
	}
    },

    findEntryPoint: function(path) {
	var pathParts = path.split('/');
	for (var pointIndex = 0; pointIndex < this.entryPoints.length; pointIndex++) {
	    var entryPoint = this.entryPoints[pointIndex];
	    var entryPointParts = entryPoint[0];

	    var bindings = {};
	    var matched = true;
	    if (pathParts.length == entryPointParts.length) {
		for (var i = 0; (i < pathParts.length); i++) {
		    if (entryPointParts[i][0] == '_') {
			bindings[entryPointParts[i].substring(1)] = pathParts[i];
		    } else {
			if (entryPointParts[i] != pathParts[i]) {
			    matched = false;
			    break;
			}
		    }
		}
		if (matched) {
		    return {entryPoint: entryPoint, bindings: bindings};
		}
	    }
	}

	return null;
    },

    reply404: function (path) {
	this.replyError(404, "text/html",
			<html>
			  <body>
			    <p>Error 404: path {path}</p>
			  </body>
			</html>);
    },

    callEntryPoint: function(path) {
	var r = this.findEntryPoint(path);
	if (r) {
	    return r.entryPoint[1](this, r.bindings);
	} else {
	    this.reply404(path);
	}
    },

    reply: function (contentType, content) {
	var resp = this.current().response;
	var os = resp.getOutputStream();
	resp.setContentType(contentType);
	os.print(content);
	os.flush();
    },

    replyText: function (content) { return this.reply("text/plain", content); },
    replyHtml: function (content) { return this.reply("text/html", content); },
    replyJson: function (content) {
	return this.reply("application/x-json", JSON.stringify(content));
    },

    replyError: function (code, contentType, content) {
	this.current().response.setStatus(code);
	return this.reply(contentType, content);
    },

    newContinuationId: function () {
	var bytes =
	    this.sha.digest(new java.lang.String(this.prng.nextInt().toString()).getBytes());
	var s = [];
	for (var i = 0; i < bytes.length; i++) {
	    var bb = ((bytes[i] + 256) & 255).toString(16).toUpperCase();
	    if (bb.length == 1) { bb = "0" + bb; }
	    s.push(bb);
	}
	return "k" + s.join("");
    },

    recordContinuation: function(k_id, k) {
	this.continuationMap()[k_id] = {
	    cont: k,
	    savedChain: this.saveStateChain()
	};
    },

    unwindToDispatcher: function () {
	this.unwinding = true;
	throw "<Savannah.unwind>";
    },

    isUnwinding: function () {
	return this.unwinding;
    },

    sendAndSuspend: function(replier) {
	var k_id = this.newContinuationId();
	var k_url = this.current().path_base + ";" + k_id;
	var k = new Continuation();
	this.recordContinuation(k_id, k);
	replier(k_url);
	this.unwindToDispatcher();
    },

    sendAndDispatch: function(replier) {
	var actionMap = {};
	var k = callCC(function (cc) { return cc });
	if (k instanceof Continuation) {
	    function procToUrl(proc) {
		var k_id = this.newContinuationId();
		var k_url = this.current().path_base + ";" + k_id;
		actionMap[k_id] = proc;
		this.recordContinuation(k_id, k);
		return k_url;
	    }
	    replier(procToUrl.bind(this));
	    this.unwindToDispatcher();
	} else {
	    var k_id = this.current().invoked_k;
	    return actionMap[k_id](k);
	}
    },

    saveStateChain: function () {
	var curr = this.current().stateChain;
	var acc = null;
	while (curr != null) {
	    acc = {
		holder: curr.holder,
		value: curr.holder.value,
		next: acc
	    };
	    curr = curr.next;
	}
	return acc;
    },

    restoreStateChain: function (savedChain) {
	var curr = savedChain;
	var acc = null;
	while (curr != null) {
	    acc = {
		holder: curr.holder,
		next: acc
	    };
	    curr.holder.value = curr.value;
	    curr = curr.next;
	}
	this.current().stateChain = acc;
    },

    withState: function (initValue, proc) {
	var h = new Savannah.StateHolder(this, initValue);
	try {
	    return proc(h);
	} finally {
	    if (!this.isUnwinding()) {
		h.forget();
	    }
	}
    },

    extractContinuationId: function(path) {
	var m = /(.*);([^?;]+)/(path);
	if (m) {
	    return [m[1], m[2]];
	} else {
	    return [path, null];
	}
    },

    handleRequest: function (req, resp) {
	var pathInfo = String(req.getRequestURI());
	if (pathInfo == null) { pathInfo = "/"; }

	var path_parts = this.extractContinuationId(pathInfo);
	this.current().path_base = path_parts[0];
	var k_id = path_parts[1];
	this.current().invoked_k = k_id;

	if (k_id) {
            // TODO: expire continuations
            var entry = this.continuationMap()[k_id];
            if (entry) {
		this.restoreStateChain(entry.savedChain);
		return entry.cont([req, resp]);
            }
	}

	this.current().stateChain = null;
	this.callEntryPoint(path_parts[0]);
    },

    ProcessGet: function (req, resp) {
	this.unwinding = false;

        var oldCurrent = this._current.get();
	var newCurrent = {
	    request: req,
	    response: resp
	};
        try {
            this._current.set(newCurrent);
            var s = req.getSession();
            var sd = s.getAttribute("savannahSessionData");
            if (sd == null) {
                sd = {};
                s.setAttribute("savannahSessionData", sd);
            }
            this.current().sessionData = sd;
	    this.handleRequest(req, resp);
        } catch (e) {
	    if (e == "<Savannah.unwind>") {
		this.unwinding = false;
	    } else {
		print("ERROR-IN-SAVANNAH-SERVLET:: " + e);
		this.replyHtml(<html>
			         <head><title>Exception in SavannahServlet</title></head>
			         <body><pre>{e.toString()}</pre></body>
			       </html>);
	    }
        }
        this._current.set(oldCurrent);
    },
};

Savannah.StateHolder = Class.create();
Savannah.StateHolder.prototype = {
    initialize: function (servlet, initValue) {
	this.value = initValue;
	this.servlet = servlet;
	this.servlet.current().stateChain = {
	    holder: this,
	    next: this.servlet.current().stateChain
	};
    },

    forget: function () {
	var prev = null;
	var curr = this.servlet.current().stateChain;
	while (curr != null) {
	    if (curr.holder == this) {
		if (prev == null) {
		    this.servlet.current().stateChain = curr.next;
		} else {
		    prev.next = curr.next;
		}
		return;
	    }
	}
	throw "Savannah.StateHolder.alreadyForgotten";
    }
}

//===========================================================================

if (typeof s != 'undefined') { s.stop(); }
sv = new Savannah.Servlet();
s = startServer(8080);
addServlet(s, "/", makeServlet({ProcessGet: sv.ProcessGet.bind(sv)}));

Bean = {
    displayProperty: function (b, selector) {
	if (typeof selector == 'function') {
	    return (selector.bind(b))();
	} else {
	    var v = b[selector];
	    if (typeof v == 'function') {
		return (v.bind(b))();
	    } else {
		return v;
	    }
	}
    },

    toTable: function (b, selectors) {
	var f = <table class="beanTable"/>;
	selectors.each(function (selector) {
			   f.table +=
			       <tr class="beanTableRow">
			         <th>{selector}</th>
			         <td>{Bean.displayProperty(b, selector)}</td>
			       </tr>;
		       });
	return f;
    },
}

function doc(title, body) {
    return <html>
	<head>
	  <title>{title}</title>
	  <link rel="stylesheet" type="text/css" href="static/style.css"/>
	</head>
	<body>{body}</body>
	</html>;
}

sv.addEntryPoint
("/",
 function (servlet, bindings) {
     print("FRESH: " + JSON.stringify(bindings));
     servlet.sendAndSuspend(function (k_url) {
				servlet.replyHtml(doc("Step 1",
						      <p>
						        <a href={k_url}>Next</a>
						        {new Date()}
						      </p>))});
     servlet.sendAndSuspend(function (k_url) {
				servlet.replyHtml(doc("Step 1.5",
						      <p>
						        <a href={k_url}>Next</a>
						        <i>Intermediate!</i>
						      </p>))});
     servlet.replyHtml(doc("Step 2",
			   <>
		             <b style="color: red;">Hello</b>
		             <p>{JSON.stringify(servlet.current())}</p>
			   </>));
 });

sv.addEntryPoint
("/bean",
 function (servlet, bindings) {
     servlet.replyHtml(doc("Bean Bindings",
			   Bean.toTable(servlet.current().request,
					["getRequestURI",
					 "getRequestURL",
					 "getPathInfo",
					])));
 });

sv.addEntryPoint
("/count",
 function (servlet, bindings) {
     var finalC = servlet.withState
     (10,
      function (c) {
	  while (servlet.sendAndDispatch
		 (function (embedUrl) {
		      servlet.replyHtml
		      (doc("Counter",
			   <>
			     <p>{c.value}</p>
			     <p>
			       <a href={embedUrl(function(){c.value++; return true})}>More</a>;
			       <a href={embedUrl(function(){c.value--; return true})}>Less</a>;
			       <a href={embedUrl(function(){return false})}>Stop</a>;
			     </p>
			   </>));
		  }))
	  {}
	  return c.value;
      });
     servlet.replyHtml(doc("Bye!", <p>Bye! {finalC}</p>));
 });

staticFiles = {
    "style.css": ["text/css", null],
    "prototype-1.4.0.js": ["text/javascript", null],
}
sv.addEntryPoint
("/static/_filename",
 function (servlet, bindings) {
     var entry = staticFiles[bindings.filename];
     if (entry) {
	 servlet.reply(entry[0], readFile(entry[1] || bindings.filename));
     } else {
	 servlet.reply404("static/" + bindings.filename);
     }
 });

