package net.yak.stalk;

import java.util.Arrays;
import java.util.Hashtable;
import java.util.Vector;
import java.util.regex.Pattern;

import net.yak.stalk.Cls.JavMeth;
import net.yak.stalk.Expr.GetLocalVar;
import net.yak.stalk.Expr.Seq;
import net.yak.stalk.Talk.Terp.Frame;
import net.yak.stalk.Talk.Terp.Visitor;

public class Pro extends Talk implements Comparable<Pro> {
	Cls cls;
	long instId;

	public Pro(Cls cls) {
		this.cls = cls;
		if (cls != null) { // This happens during bootup.
			++cls.countInstances;
			this.instId = cls.countInstances;
		}
	}
	public String toString() {
		return fmt("~%s~%s~", cls.name, this.instId);
	}
	public void visit(Visitor v) {
		v.visitPro(this);
	}
	Terp terp() {
		return cls.terp;
	}
	Num asNum() {
		return null;
	}
	Str asStr() {
		return null;
	}
	Cls asCls() {
		return null;
	}
	Blk asBlk() {
		return null;
	}
	Undefined asNil() {
		return null;
	}
	Usr asUsr() {
		return null;
	}
	Boolean asBool() {
		return null;
	}
	Vec asVec() {
		return null;
	}
	Map asMap() {
		return null;
	}
	Pro boolPro(Terp t, boolean x) {
		return x ? t.instTrue : t.instFalse;
	}
	@SuppressWarnings("unchecked")
	Comparable<Object> innerObject() {
		return (Comparable<Object>) (Object) this;
	}
	public int hashCode() {
		Object innerThis = this.innerObject();
		if (innerThis == this) {
			// Avoid infinite recursion.
			return System.identityHashCode(this);
		} else {
			return innerObject().hashCode();
		}
	}
	// TODO: How does equals() differ from #eq: ?
	// Thinking ahead to having Array Programming operators with Vec,
	// Should we define #equals: to mean equals()
	// and perhaps #same: to mean operator==
	// but only allow overrides and Double Dispatch on #eq: ?
	public boolean equals(Object obj) {
		if (obj instanceof Pro) {
			Pro that = (Pro) obj;
			if (this.cls != that.cls)
				return false;
			Object innerThis = this.innerObject();
			Object innerThat = that.innerObject();
			if (innerThis != this && innerThat != that) {
				// Both inner objects were different; compare them.
				return this.innerObject().equals(that.innerObject());
			} else {
				// Avoid infinite recursion.
				return this == that;
			}
		} else {
			return false;
		}
	}
	public int compareTo(Pro that) {
		if (this.cls != that.cls) {
			return this.cls.name.compareTo(that.cls.name);
		}
		Comparable<Object> a = this.innerObject();
		Comparable<Object> b = that.innerObject();
		if (a != (Object) this) {
			// Not Equal because innerObject returned Value-like objects.
			return a.compareTo(b);
		} else {
			return new Integer(System.identityHashCode(this))
					.compareTo(new Integer(System.identityHashCode(that)));
		}
	}

	int toNearestInt() {
		Num num = this.asNum();
		if (num == null) {
			toss("Object is a %s, not a Num", this.cls.name);
		}
		int i = (int) Math.floor(num.num + 0.5); // closest int
		return i;
	}

	static void addBuiltinMethodsForPro(Terp terp) {
		terp.tPro.addMethod(new JavMeth(terp, "class", "cls") {
			@Override
			public Pro apply(Frame f, Pro r, Pro[] args) {
				return r.cls;
			}
		});
	}

	public static class Obj extends Pro {
		public Obj(Cls cls) {
			super(cls);
		}
		Obj asObj() {
			return this;
		}
		static void addBuiltinMethodsForObj(final Terp terp) {
			terp.tObj.addMethod(new JavMeth(terp, "str", "") {
				public Pro apply(Frame f, Pro r, Pro[] args) {
					assert args.length == 0;
					return terp.newStr(r.toString());
				}
			});
			terp.tObj.addMethod(new JavMeth(terp, "doit:", "") {
				public Pro apply(Frame f, Pro r, Pro[] args) {
					assert args.length == 1;
					String code = args[0].asStr().str;
					say("DOIT <- %s", code);
					Expr expr = Parser.parse(terp, code);
					assert expr != null;
					Pro z = expr.eval(terp.newFrame(null, r));
					if (z == null) z = terp.newStr("<NULL>");
					say("DOIT -> %s", z);
					return z;
				}
			});
			terp.tObj.addMethod(new JavMeth(terp, "isInstOf", "inst") {
				public Pro apply(Frame f, Pro r, Pro[] args) {
					assert args.length == 1;
					Cls query = args[0].asCls();
					if (query == null) {
						toss("isInstOf: expected a Cls as it arg");
					}
					for (Cls p = r.cls; p != null; p = query.supercls) {
						if (p == query) {
							return terp.instTrue;
						}
					}
					return terp.instFalse;
				}
			});
			terp.tObj.addMethod(new JavMeth(terp, "equals", "") {
				public Obj apply(Frame f, Pro r, Pro[] args) {
					assert args.length == 1;
					return this.equals(args[0]) ? terp.instTrue
							: terp.instFalse;
				}
			});
			terp.tObj.addMethod(new JavMeth(terp, "isSameAs", "same") {
				public Obj apply(Frame f, Pro r, Pro[] args) {
					assert args.length == 1;
					return this == args[0] ? terp.instTrue : terp.instFalse;
				}
			});
		}
	}

	public final static class Sys extends Obj {
		public Sys(Terp terp) {
			super(terp.tSys);
		}

		static void addBuiltinMethodsForSys(final Terp terp) {
			terp.tSysCls.addMethod(new JavMeth(terp, "globalMap", "") {
				public Pro apply(Frame f, Pro r, Pro[] args) {
					assert args.length == 0;
					Pro[] arr = new Pro[terp.glob.dict.size() * 2];
					int i = 0;
					for (String k : terp.glob.dict.keySet()) {
						arr[i] = terp.newStr(k);
						arr[i + 1] = terp.glob.dict.get(k);
						i += 2;
					}
					return terp.newMap(arr);
				}
			});
		}
	}

	public final static class Undefined extends Obj { // Nil
		public Undefined(Cls cls) {
			super(cls);
		}
		Undefined asNil() {
			return this;
		}
		public String toString() {
			return "Nil";
		}
		public void visit(Visitor v) {
			v.visitUndefined(this);
		}
		static void addBuiltinMethodsForFal(final Terp terp) {
			terp.tFalse.addMethod(new JavMeth(terp, "ifNilObj:", "ni") {
				public Pro apply(Frame f, Pro r, Pro[] args) {
					assert args.length == 1;
					Blk blk = args[0].asBlk();
					// TODO: Create child frame.
					return blk.body.eval(f);
				}
			});
			terp.tFalse.addMethod(new JavMeth(terp, "ifNotNil:", "nn") {
				public Pro apply(Frame f, Pro r, Pro[] args) {
					return terp.instNil;
				}
			});
		}
	}

	// TODO: shouldn't this be "class FalCls" instead?
	public final static class False extends Obj { // False
		public False(Cls cls) {
			super(cls);
		}
		Boolean asBool() {
			return Boolean.FALSE;
		}
		public String toString() {
			return "Fal";
		}
		public void visit(Visitor v) {
			v.visitFalse(this);
		}
		static void addBuiltinMethodsForFal(final Terp terp) {
			terp.tFalse.addMethod(new JavMeth(terp, "ifNo:", "no:") {
				public Pro apply(Frame f, Pro r, Pro[] args) {
					assert args.length == 1;
					Blk blk = args[0].asBlk();
					// TODO: Create child frame.
					return blk.body.eval(f);
				}
			});
			terp.tFalse.addMethod(new JavMeth(terp, "ifYe:ifNo:", "ye:no:") {
				public Pro apply(Frame f, Pro r, Pro[] args) {
					assert args.length == 2;
					Blk blk = args[1].asBlk();
					// TODO: Create child frame.
					return blk.body.eval(f);
				}
			});
			terp.tFalse.addMethod(new JavMeth(terp, "ifNo:ifYe:", "no:ye:") {
				public Pro apply(Frame f, Pro r, Pro[] args) {
					assert args.length == 2;
					Blk blk = args[0].asBlk();
					// TODO: Create child frame.
					return blk.body.eval(f);
				}
			});
			terp.tFalse.addMethod(new JavMeth(terp, "ifYe", "ye:") {
				public Pro apply(Frame f, Pro r, Pro[] args) {
					// Don't eval the block. Return Nil.
					return terp.instNil;
				}
			});
			terp.tFalse.addMethod(new JavMeth(terp, "not", "") {
				public Pro apply(Frame f, Pro r, Pro[] args) {
					return terp.instTrue;
				}
			});
		}
	}

	public final static class True extends Obj { // True
		public True(Cls cls) {
			super(cls);
		}
		Boolean asBool() {
			return Boolean.TRUE;
		}
		public String toString() {
			return "Tru";
		}
		public void visit(Visitor v) {
			v.visitTrue(this);
		}
		static void addBuiltinMethodsForTru(final Terp terp) {
			terp.tTrue.addMethod(new JavMeth(terp, "ifYe", "ye:") {
				public Pro apply(Frame f, Pro r, Pro[] args) {
					assert args.length == 1;
					Blk blk = args[0].asBlk();
					// TODO: Create child frame.
					return blk.body.eval(f);
				}
			});
			terp.tTrue.addMethod(new JavMeth(terp, "ifYe:ifNo:", "ye:no:") {
				public Pro apply(Frame f, Pro r, Pro[] args) {
					assert args.length == 2;
					Blk blk = args[0].asBlk();
					// TODO: Create child frame.
					return blk.body.eval(f);
				}
			});
			terp.tTrue.addMethod(new JavMeth(terp, "ifNo:ifYe:", "no:ye:") {
				public Pro apply(Frame f, Pro r, Pro[] args) {
					assert args.length == 2;
					Blk blk = args[1].asBlk();
					// TODO: Create child frame.
					return blk.body.eval(f);
				}
			});
			terp.tTrue.addMethod(new JavMeth(terp, "ifNo", "no:") {
				public Pro apply(Frame f, Pro r, Pro[] args) {
					// Don't eval the block. Return Nil.
					return terp.instNil;
				}
			});
			terp.tTrue.addMethod(new JavMeth(terp, "not", "") {
				public Pro apply(Frame f, Pro r, Pro[] args) {
					return terp.instFalse;
				}
			});
		}
	}

	public final static class Blk extends Obj {
		Expr body;
		Frame f;

		public Blk(Expr body, Frame f) {
			super(f.terp().tBlk);
			this.body = body;
			this.f = f;
		}
		Blk asBlk() {
			return this;
		}
		public String toString() {
			return fmt("[ %s ] ", body);
		}

		static void addBuiltinMethodsForBlk(final Terp terp) {

			// "any" and "or_" are the same.
			terp.tBlk.addMethod(new JavMeth(terp, "any", "",
					"Return true if any member evaluates true.") {
				public Pro apply(Frame _, Pro r, Pro[] args) {
					Blk blk = r.asBlk();
					// Notice the block runs in its frame, not the caller's.
					Expr.Seq seq = (Seq) blk.body;
					Pro[] arr = new Pro[seq.body.length];
					for (int i = 0; i < arr.length; i++) {
						Pro x = seq.body[i].eval(blk.f);
						if (x == terp.instTrue)
							return terp.instTrue;
					}
					return terp.instFalse;
				}
			});
			terp.tBlk.addMethod(new JavMeth(terp, "or_", "",
					"Return true if any member evaluates true.") {
				public Pro apply(Frame _, Pro r, Pro[] args) {
					Blk blk = r.asBlk();
					// Notice the block runs in its frame, not the caller's.
					Expr.Seq seq = (Seq) blk.body;
					Pro[] arr = new Pro[seq.body.length];
					for (int i = 0; i < arr.length; i++) {
						Pro x = seq.body[i].eval(blk.f);
						if (x == terp.instTrue)
							return terp.instTrue;
					}
					return terp.instFalse;
				}
			});
			// "all" & "and" are the same.
			terp.tBlk.addMethod(new JavMeth(terp, "all", "",
					"Return false if any member evaluates untrue.") {
				public Pro apply(Frame _, Pro r, Pro[] args) {
					Blk blk = r.asBlk();
					// Notice the block runs in its frame, not the caller's.
					Expr.Seq seq = (Seq) blk.body;
					Pro[] arr = new Pro[seq.body.length];
					for (int i = 0; i < arr.length; i++) {
						Pro x = seq.body[i].eval(blk.f);
						if (x != terp.instTrue)
							return terp.instFalse;
					}
					return terp.instTrue;
				}
			});
			terp.tBlk.addMethod(new JavMeth(terp, "and", "",
					"Return false if any member evaluates untrue.") {
				public Pro apply(Frame _, Pro r, Pro[] args) {
					Blk blk = r.asBlk();
					// Notice the block runs in its frame, not the caller's.
					Expr.Seq seq = (Seq) blk.body;
					Pro[] arr = new Pro[seq.body.length];
					for (int i = 0; i < arr.length; i++) {
						Pro x = seq.body[i].eval(blk.f);
						if (x != terp.instTrue)
							return terp.instFalse;
					}
					return terp.instTrue;
				}
			});
			terp.tBlk.addMethod(new JavMeth(terp, "vec", "v",
					"Evalute members of the block to make a Vec.") {
				public Pro apply(Frame _, Pro r, Pro[] args) {
					Blk blk = r.asBlk();
					// Notice the block runs in its frame, not the caller's.
					Expr.Seq seq = (Seq) blk.body;
					Pro[] arr = new Pro[seq.body.length];
					for (int i = 0; i < arr.length; i++) {
						arr[i] = seq.body[i].eval(blk.f);
					}
					return terp.newVec(arr);
				}
			});
			terp.tBlk.addMethod(new JavMeth(terp, "map", "m",
					"Evalute members of the block to make a Map.  "
							+ "Alternating elements are keys and values.") {
				public Pro apply(Frame _, Pro r, Pro[] args) {
					Blk blk = r.asBlk();
					// Notice the block runs in its frame, not the caller's.
					Expr.Seq seq = (Seq) blk.body;
					Pro[] arr = new Pro[seq.body.length];
					for (int i = 0; i < arr.length; i += 2) {
						arr[i] = seq.body[i].eval(blk.f);
						arr[i + 1] = seq.body[i + 1].eval(blk.f);
					}
					return terp.newMap(arr);
				}
			});
			terp.tBlk.addMethod(new JavMeth(terp, "run", "r",
					"Evaluate the block.") {
				public Pro apply(Frame _, Pro r, Pro[] args) {
					Blk blk = r.asBlk();
					// Notice the block runs in its frame, not the caller's.
					return blk.body.eval(blk.f);
				}
			});
			terp.tBlk.addMethod(new JavMeth(terp, "runArg:", "ra",
					"Run the block with one argument.") {
				public Pro apply(Frame _, Pro r, Pro[] args) {
					Blk blk = r.asBlk();
					assert args.length == 1;
					assert blk.body instanceof Seq;
					Seq seq = (Seq) blk.body;
					assert seq.body.length > 0;
					if (seq.body[0] instanceof GetLocalVar) {
						GetLocalVar glv = (GetLocalVar) seq.body[0];
						blk.f.dict.put(glv.name, args[0]);
					} else {
						toss("Block does not declare an arg variable");
					}
					// Notice the block runs in its frame, not the caller's.
					return blk.evalWithArg(args[0]);
				}
			});
			terp.tBlk.addMethod(new JavMeth(terp, "runArg:with:", "ra:wi:",
					"Run the block with two arguments.") {
				public Pro apply(Frame _, Pro r, Pro[] args) {
					Blk blk = r.asBlk();
					assert args.length == 2;
					return blk.evalWithArgs2(args[0], args[1]);
				}
			});
		}
		Pro evalWithArg(Pro arg0) {
			assert this.body instanceof Seq;
			Seq seq = (Seq) this.body;
			if (seq.body.length > 0 && seq.body[0] instanceof GetLocalVar) {
				GetLocalVar glv = (GetLocalVar) seq.body[0];
				this.f.dict.put(glv.name, arg0);
			} else {
				toss("Block does not declare an arg variable");
			}
			// Notice the block runs in its frame, not the caller's.
			return this.body.eval(this.f);
		}
		Pro evalWithArgs2(Pro arg0, Pro arg1) {
			assert this.body instanceof Seq;
			Seq seq = (Seq) this.body;
			if (seq.body.length > 0 && seq.body[0] instanceof GetLocalVar) {
				GetLocalVar glv = (GetLocalVar) seq.body[0];
				this.f.dict.put(glv.name, arg0);
			} else {
				toss("Block does not declare an arg variable");
			}
			if (seq.body.length > 1 && seq.body[1] instanceof GetLocalVar) {
				GetLocalVar glv = (GetLocalVar) seq.body[1];
				this.f.dict.put(glv.name, arg1);
			} else {
				toss("Block does not declare a second arg variable");
			}
			// Notice the block runs in its frame, not the caller's.
			return this.body.eval(this.f);
		}
	}

	public final static class Num extends Obj {
		double num;

		Num(Terp t, double num) {
			super(t.tNum);
			this.num = num;
		}
		Num asNum() {
			return this;
		}
		public String toString() {
			return fmt("%s", num);
		}
		@SuppressWarnings("unchecked")
		Comparable innerObject() {
			return new Double(num);
		}
		public void visit(Visitor v) {
			v.visitNum(this);
		}

		abstract static class BinaryNumPredMeth extends JavMeth {
			BinaryNumPredMeth(Terp terp, String name) {
				super(terp, name, null, "");
			}
			public Pro apply(Frame f, Pro r, Pro[] args) {
				assert args.length == 1;
				Num s = r.asNum();
				Num a = args[0].asNum();
				if (s == null) {
					toss("Receiver of Num relop not a Num");
				}
				if (a == null) {
					toss("Argument of Num relop not a Num");
				}
				return boolPro(f.terp(), compute(s.num, a.num));
			}
			abstract boolean compute(double snum, double anum);
		}

		abstract static class BinaryNumMeth extends JavMeth {
			BinaryNumMeth(Terp terp, String name) {
				super(terp, name, null, "");
			}
			public Pro apply(Frame f, Pro r, Pro[] args) {
				assert args.length == 1;
				Num s = r.asNum();
				Num a = args[0].asNum();
				if (s == null) {
					toss("Receiver of Num binop not a Num");
				}
				if (a == null) {
					toss("Argument of Num binop not a Num");
				}
				return new Num(f.terp(), compute(s.num, a.num));
			}
			abstract double compute(double snum, double anum);
		}

		abstract static class UnaryNumMeth extends JavMeth {
			UnaryNumMeth(Terp terp, String name) {
				super(terp, name, null, "");
			}
			public Pro apply(Frame f, Pro r, Pro[] args) {
				assert args.length == 0;
				Num s = r.asNum();
				if (s == null) {
					toss("Receiver of Num unary op not a Num");
				}
				return new Num(f.terp(), compute(s.num));
			}
			abstract double compute(double anum);
		}

		static void addBuiltinMethodsForNum(Terp terp) {
			terp.tNum
					.addMethod(new JavMeth(
							terp,
							"doZeroTo:",
							"do:",
							"Iterate the argument block this many times, with one argument ranging from 0 up to, not including, self.") {
						public Pro apply(Frame f, Pro r, Pro[] args) {
							Num num = r.asNum();
							assert args.length == 1;
							Blk blk = args[0].asBlk();
							double stop = num.num - 0.5;
							for (double i = 0; i < stop; i++) {
								blk.evalWithArg(new Num(f.terp(), i));
							}
							return f.terp().instNil;
						}
					});

			// TODO: move these up to Obj.
			terp.tNum.addMethod(new BinaryNumPredMeth(terp, "eq:") {
				boolean compute(double snum, double anum) {
					return snum == anum;
				}
			});
			terp.tNum.addMethod(new BinaryNumPredMeth(terp, "ne:") {
				boolean compute(double snum, double anum) {
					return snum != anum;
				}
			});
			terp.tNum.addMethod(new BinaryNumPredMeth(terp, "lt:") {
				boolean compute(double snum, double anum) {
					return snum < anum;
				}
			});
			terp.tNum.addMethod(new BinaryNumPredMeth(terp, "le:") {
				boolean compute(double snum, double anum) {
					return snum <= anum;
				}
			});
			terp.tNum.addMethod(new BinaryNumPredMeth(terp, "gt:") {
				boolean compute(double snum, double anum) {
					return snum > anum;
				}
			});
			terp.tNum.addMethod(new BinaryNumPredMeth(terp, "ge:") {
				boolean compute(double snum, double anum) {
					return snum >= anum;
				}
			});

			terp.tNum.addMethod(new BinaryNumMeth(terp, "pl:") {
				double compute(double snum, double anum) {
					return snum + anum;
				}
			});
			terp.tNum.addMethod(new BinaryNumMeth(terp, "mi:") {
				double compute(double snum, double anum) {
					return snum - anum;
				}
			});
			terp.tNum.addMethod(new BinaryNumMeth(terp, "ti:") {
				double compute(double snum, double anum) {
					return snum * anum;
				}
			});
			terp.tNum.addMethod(new BinaryNumMeth(terp, "fdiv:") {
				double compute(double snum, double anum) {
					return snum / anum;
				}
			});
			terp.tNum.addMethod(new BinaryNumMeth(terp, "idiv:") {
				double compute(double snum, double anum) {
					return (long) snum / (long) anum;
				}
			});
			terp.tNum.addMethod(new BinaryNumMeth(terp, "imod:") {
				double compute(double snum, double anum) {
					return (long) snum % (long) anum;
				}
			});
			terp.tNum.addMethod(new UnaryNumMeth(terp, "neg") {
				double compute(double snum) {
					return -snum;
				}
			});
			terp.tNum.addMethod(new UnaryNumMeth(terp, "sgn") {
				double compute(double snum) {
					return snum < 0 ? -1 : snum > 0 ? 1 : 0;
				}
			});
			terp.tNum.addMethod(new UnaryNumMeth(terp, "int") {
				double compute(double snum) {
					return (long) snum;
				}
			});
		}
	}

	public final static class Buf extends Obj {
		StringBuffer buf;

		Buf(Terp t) {
			super(t.tBuf);
			this.buf = new StringBuffer();
		}
		Buf(Terp t, String s) {
			super(t.tBuf);
			this.buf = new StringBuffer(s);
		}
		public String toString() {
			return fmt("~Buf~'%s'~", buf.toString());
		}
		static void addBuiltinMethodsForBuf(final Terp terp) {
			terp.tBuf.addMethod(new JavMeth(terp, "str", "s",
					"Get Str from Buf.") {
				public Pro apply(Frame f, Pro r, Pro[] args) {
					assert args.length == 0;
					Buf self = (Buf) r;
					return terp.newStr(self.buf.toString());
				}
			});
			terp.tBuf.addMethod(new JavMeth(terp, "append:", "ap",
					"Append the argument to the Buf, modifying self.") {
				public Pro apply(Frame f, Pro r, Pro[] args) {
					assert args.length == 1;
					Buf self = (Buf) r;
					Str a = args[0].asStr();
					if (a == null) {
						toss("Argument of Buf>>ap: not a Str");
					}
					self.buf.append(a.str);
					return r;
				}
			});
			terp.tBufCls.addMethod(new JavMeth(terp, "new", "",
					"Create new empty Buf.") {
				public Pro apply(Frame f, Pro r, Pro[] args) {
					assert args.length == 0;
					return new Buf(terp);
				}
			});
			terp.tBufCls.addMethod(new JavMeth(terp, "append:", "ap",
					"Append the argument to the Buf, modifying self.") {
				public Pro apply(Frame f, Pro r, Pro[] args) {
					assert args.length == 1;
					Str a = args[0].asStr();
					if (a == null) {
						toss("Argument of Buf>>ap: not a Str");
					}
					return new Buf(terp, a.str);
				}
			});
		}
	}

	public final static class Str extends Obj {
		String str;

		Str(Terp t, String str) {
			super(t.tStr);
			this.str = str;
		}
		Str asStr() {
			return this;
		}
		public String toString() {
//			if (str == null) {
//				return "<*BAD_NULL_STR_IN_STR*>";
//			}
			return fmt("'%s'", str.replaceAll("'", "''"));
		}
		@SuppressWarnings("unchecked")
		Comparable innerObject() {
			return str;
		}
		public void visit(Visitor v) {
			v.visitStr(this);
		}

		abstract static class BinaryStrPredMeth extends JavMeth {
			BinaryStrPredMeth(Terp terp, String name) {
				super(terp, name, null, "");
			}
			public Pro apply(Frame f, Pro r, Pro[] args) {
				assert args.length == 1;
				Str s = r.asStr();
				Str a = args[0].asStr();
				if (s == null) {
					toss("Receiver of Str relop not a Str");
				}
				if (a == null) {
					toss("Argument of Str relop not a Str");
				}
				return boolPro(f.terp(), compute(s.str, a.str));
			}
			abstract boolean compute(String sstr, String astr);
		}

		static void addBuiltinMethodsForStr(final Terp terp) {
			terp.tStr.addMethod(new JavMeth(terp, "spli:", "",
					"Split string (on given Regexp string) into a Vec.") {
				public Pro apply(Frame f, Pro r, Pro[] args) {
					assert args.length == 1;
					Str s = r.asStr();
					Str a = args[0].asStr();
					if (s == null) {
						toss("Receiver of Str>>spli: not a Str");
					}
					if (a == null) {
						toss("Argument of Str>>spli: not a Str");
					}
					String[] parts = s.str.split(a.str);
					Pro[] arr = new Pro[parts.length];
					for (int i = 0; i < parts.length; i++) {
						arr[i] = terp.newStr(parts[i]);
					}
					return terp.newVec(arr);
				}
			});
			terp.tStr
					.addMethod(new JavMeth(terp, "append:", "ap",
							"Append the argument (as a string) to the string, modifying self.") {
						public Pro apply(Frame f, Pro r, Pro[] args) {
							assert args.length == 1;
							Str s = r.asStr();
							Str a = args[0].asStr();
							if (s == null) {
								toss("Receiver of Str>>ap: not a Str");
							}
							if (a == null) {
								toss("Argument of Str>>ap: not a Str");
							}
							return new Str(terp, s.str + a.str);
						}
					});
			terp.tStr.addMethod(new JavMeth(terp, "substr:to:", "ss:to:",
					"Substring starting at first index, "
							+ "ending before second index, like Java substr.") { // Substring
						public Pro apply(Frame f, Pro r, Pro[] args) {
							assert args.length == 2;
							Str s = r.asStr();
							assert (s != null);
							int a = args[0].toNearestInt();
							int b = args[1].toNearestInt();
							return new Str(terp, s.str.substring(a, b));
						}
					});
			terp.tStr.addMethod(new JavMeth(terp, "toLower", "low",
					"Convert string to lowercase.") {
				public Pro apply(Frame f, Pro r, Pro[] args) {
					return new Str(terp, r.asStr().str.toLowerCase());
				}
			});
			terp.tStr.addMethod(new JavMeth(terp, "toUpper", "upp",
					"Convert string to uppercase.") {
				public Pro apply(Frame f, Pro r, Pro[] args) {
					return new Str(terp, r.asStr().str.toUpperCase());
				}
			});
			terp.tStr.addMethod(new JavMeth(terp, "trimWhite", "trimw",
					"Trim whitespace from front and back.") {
				public Pro apply(Frame f, Pro r, Pro[] args) {
					return new Str(terp, r.asStr().str.trim());
				}
			});
			// TODO: move these up to Obj.
			terp.tStr.addMethod(new BinaryStrPredMeth(terp, "eq:") {
				boolean compute(String sstr, String astr) {
					return sstr.compareTo(astr) == 0;
				}
			});
			terp.tStr.addMethod(new BinaryStrPredMeth(terp, "ne:") {
				boolean compute(String sstr, String astr) {
					return sstr.compareTo(astr) != 0;
				}
			});
			terp.tStr.addMethod(new BinaryStrPredMeth(terp, "lt:") {
				boolean compute(String sstr, String astr) {
					return sstr.compareTo(astr) < 0;
				}
			});
			terp.tStr.addMethod(new BinaryStrPredMeth(terp, "le:") {
				boolean compute(String sstr, String astr) {
					return sstr.compareTo(astr) <= 0;
				}
			});
			terp.tStr.addMethod(new BinaryStrPredMeth(terp, "gt:") {
				boolean compute(String sstr, String astr) {
					return sstr.compareTo(astr) > 0;
				}
			});
			terp.tStr.addMethod(new BinaryStrPredMeth(terp, "ge:") {
				boolean compute(String sstr, String astr) {
					return sstr.compareTo(astr) >= 0;
				}
			});
			terp.tStr.addMethod(new BinaryStrPredMeth(terp, "starts:") {
				boolean compute(String sstr, String astr) {
					return sstr.startsWith(astr);
				}
			});
			terp.tStr.addMethod(new BinaryStrPredMeth(terp, "ends:") {
				boolean compute(String sstr, String astr) {
					return sstr.endsWith(astr);
				}
			});
			terp.tStr.addMethod(new BinaryStrPredMeth(terp, "matchp:") {
				boolean compute(String sstr, String astr) {
					return Pattern.matches(astr, sstr);
				}
			});
		}
	}

	public final static class Vec extends Obj {
		Vector<Pro> vec;

		Vec(Terp t) {
			super(t.tVec);
			this.vec = new Vector<Pro>();
		}
		Vec asVec() {
			return this;
		}
		public String toString() {
			StringBuffer sb = new StringBuffer("[ ");
			for (int i = 0; i < vec.size(); i++) {
				sb
						.append(vec.get(i) == null ? "<NULL>" : vec.get(i)
								.toString());
				sb.append(" . ");
			}
			sb.append("] vec ");
			return sb.toString();
		}
		public void visit(Visitor v) {
			v.visitVec(this);
		}
		int toNearestIndex(Pro a) {
			final int vlen = vec.size();
			if (vlen == 0) {
				toss("Cannot index into empty Vec");
			}
			Num num = a.asNum();
			if (num == null) {
				toss("Index is a %s, not a Num, in <Vec at:>", a.cls.name);
			}
			int i = (int) Math.floor(num.num + 0.5); // closest int
			i = ((i % vlen) + vlen) % vlen; // Positive-only Modulus
			return i;
		}
		static void addBuiltinMethodsForVec(final Terp terp) {
			terp.tVec.addMethod(new JavMeth(terp, "len", null,
					"Length of the Vec.") {
				public Num apply(Frame f, Pro r, Pro[] args) {
					return new Num(terp, r.asVec().vec.size());
				}
			});
			terp.tVec.addMethod(new JavMeth(terp, "at:", null,
					"Item at given index") {
				public Pro apply(Frame f, Pro r, Pro[] args) {
					Vec self = r.asVec();
					return self.vec.get(self.toNearestIndex(args[0]));
				}
			});
			terp.tVec.addMethod(new JavMeth(terp, "at:pu:", "", "") {
				public Pro apply(Frame f, Pro r, Pro[] args) {
					Vec self = r.asVec();
					self.vec.set(self.toNearestIndex(args[0]), args[1]);
					return r;
				}
			});
			terp.tVec.addMethod(new JavMeth(terp, "append:", "ap", "") {
				public Pro apply(Frame f, Pro r, Pro[] args) {
					Vec self = r.asVec();
					self.vec.add(args[0]);
					return r;
				}
			});
			terp.tVec.addMethod(new JavMeth(terp, "setLen:", "sl", "") {
				public Pro apply(Frame f, Pro r, Pro[] args) {
					Vec self = r.asVec();
					int oldSize = self.vec.size();
					Num num = args[0].asNum();
					if (num == null) {
						toss("Expected argument to be a Num; got a <%s>.",
								args[0].cls.name);
					}
					int newSize = (int) Math.floor(num.num + 0.5); // closest
					// int
					self.vec.setSize(newSize);
					for (int i = oldSize; i < newSize; i++) {
						self.vec.set(i, terp.tUndefined);
					}
					return r;
				}
			});
			terp.tVec.addMethod(new JavMeth(terp, "doWithEach:", "do",
					"Iterate the block with one argument, "
							+ "for each item in the Vec.") {
				public Pro apply(Frame f, Pro r, Pro[] args) {
					Vec vec = r.asVec();
					assert args.length == 1;
					Blk blk = args[0].asBlk();
					int stop = vec.vec.size();
					for (int i = 0; i < stop; i++) {
						blk.evalWithArg(vec.vec.get(i));
					}
					return terp.instNil;
				}
			});
			terp.tVecCls.addMethod(new JavMeth(terp, "new", "",
					"Create a new empty Vec object.") {
				public Pro apply(Frame f, Pro r, Pro[] args) {
					return new Vec(terp);
				}
			});
			terp.tVecCls.addMethod(new JavMeth(terp, "append:", "ap",
					"Create a new Vec of length 1 with the given item in it."
							+ "  Same as 'new append:'.") {
				// Shortcut for "new append:"
				public Pro apply(Frame f, Pro r, Pro[] args) {
					Vec vec = new Vec(terp);
					vec.vec.add(args[0]);
					return vec;
				}
			});
		}
	}

	public final static class Map extends Obj {
		Hashtable<Pro, Pro> map;

		Map(Terp t) {
			super(t.tMap);
			this.map = new Hashtable<Pro, Pro>();
		}
		Map asMap() {
			return this;
		}
		public String toString() {
			Assoc[] aarr = sortedAssocs();
			StringBuffer sb = new StringBuffer("[ ");
			for (Assoc a : aarr) {
				sb.append(a.key.toString());
				sb.append(" . ");
				sb.append(a.value.toString());
				sb.append(" . ");
			}
			sb.append("] map ");
			return sb.toString();
		}
		public void visit(Visitor v) {
			v.visitMap(this);
		}
		Assoc[] sortedAssocs() {
			Assoc[] z = new Assoc[map.size()];
			int i = 0;
			for (Pro key : map.keySet()) {
				z[i] = new Assoc(key, map.get(key));
				++i;
			}
			Arrays.sort(z);
			return z;
		}

		static void addBuiltinMethodsForMap(final Terp terp) {
			terp.tMap.addMethod(new JavMeth(terp, "len", "",
					"Number of entries in the Map.") {
				public Num apply(Frame f, Pro r, Pro[] args) {
					return new Num(terp, r.asMap().map.size());
				}
			});
			terp.tMap.addMethod(new JavMeth(terp, "dir", "",
					"Number of entries in the Map.") {
				public Vec apply(Frame f, Pro r, Pro[] args) {
					Vec z = terp.newVec(new Vec[0]);
					for (Assoc aa : r.asMap().sortedAssocs()) {
						z.vec.add(aa.key);
					}
					return z;
				}
			});
			terp.tMap
					.addMethod(new JavMeth(terp, "at:", "",
							"Return the value stored at the given key, or Nil if it is missing.") {
						public Pro apply(Frame f, Pro r, Pro[] args) {
							Map self = r.asMap();
							Pro z = self.map.get(args[0]);
							if (z == null) {
								return terp.instNil;
							}
							return z;
						}
					});
			terp.tMap.addMethod(new JavMeth(terp, "at:pu:", "", "") {
				public Pro apply(Frame f, Pro r, Pro[] args) {
					Map self = r.asMap();
					self.map.put(args[0], args[1]);
					return r;
				}
			});
			terp.tMapCls.addMethod(new JavMeth(terp, "new", "",
					"Create a new empty Map object.") {
				public Pro apply(Frame f, Pro r, Pro[] args) {
					return new Map(terp);
				}
			});
		}
	}

	public final static class Assoc extends Obj {
		// Assoc is not full-fledged; it was just for sorting.
		Pro key;
		Pro value;

		public Assoc(Pro a, Pro b) {
			super(a.terp().tAssoc);
			this.key = a;
			this.value = b;
		}
		public void visit(Visitor v) {
			v.visitAssoc(this);
		}
		public String toString() {
			return fmt("~ProProAssoc~%s(%s,%s)", instId, key, value);
		}
		@SuppressWarnings("unchecked")
		Comparable innerObject() {
			// Value comparisons are only based on the key.
			return key;
		}
	}

}
