package net.yak.stalk;

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

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

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

    public Pro(Cls cls) {
        this.cls = cls;
        if (cls == null) { // null cls happens during bootup.
            this.instVars = emptyPros;
            this.instId = 0;
        } else {
            ++cls.countInstances;
            this.instId = cls.countInstances;
            int allVarsSize = cls.allVars.size();
            if (allVarsSize == 0) {
                this.instVars = emptyPros;
            } else {
                this.instVars = new Pro[allVarsSize];
                for (int i = 0; i < allVarsSize; i++) {
                    this.instVars[i] = cls.terp.instNil;
                }
            }
        }
    }
    public String toString() {
        return fmt("%s~%s", cls.name, this.instId);
    }
    public void visit(Visitor v) {
        v.visitPro(this);
    }
    Terp terp() {
        return cls.terp;
    }
    Obj asObj() {
        return null;
    }
    Num asNum() {
        return null;
    }
    Str asStr() {
        return null;
    }
    Cls asCls() {
        return null;
    }
    Blk asBlk() {
        return null;
    }
    Undefined asNil() {
        return null;
    }
    Usr asUsr() {
        return null;
    }
    Vec asVec() {
        return null;
    }
    Map asMap() {
        return null;
    }
    // TODO: Some of the following should be down at Obj.
    // Pro should toss subclassResponsibility for anything it can.
    // No one will care, until somebody uses Pro.
    
    // Most objects are true.
    // nil and Nums that round to 0 are false.
    boolean truth() {
        say("Object <%s> class %s is TRUE in Obj::truth.", this, cls.name);
        return true;
    }
    Pro boolObj(boolean x) {
        return x ? terp().instTrue : terp().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()
    // 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;
        }
        Pro eval(String code) {
            say("EVAL <%s> <<< <%s>", this, code);
            Expr.Top top = Parser.parseMethod(cls, "__eval__", code);
            Pro z = top.eval(cls.terp.newFrame(null, this, top));
            assert z != null : fmt("Null result in %s.eval <%s>", this, code);
            say("EVAL <%s> >>> <%s>", this, z);
            return z;
        }
        static void addBuiltinMethodsForObj(final Terp terp) {
            terp.tObj.addMethod(new JavMeth(terp, "log", "") {
                public Pro apply(Frame f, Pro r, Pro[] args) {
                    say("#LOG# %s", r);
                    return r;
                }
            });
            terp.tObj.addMethod(new JavMeth(terp, "log:", "") {
                public Pro apply(Frame f, Pro r, Pro[] args) {
                    say("#LOG# %s; %s", r, args[0]);
                    return r;
                }
            });
            terp.tObj.addMethod(new JavMeth(terp, "hash", "") {
                public Pro apply(Frame f, Pro r, Pro[] args) {
                    return terp.newNum(System.identityHashCode(r.innerObject()));
                }
            });
            terp.tObj.addMethod(new JavMeth(terp, "not", "") {
                public Pro apply(Frame f, Pro r, Pro[] args) {
                    return boolObj(r.truth() == false);
                }
            });
            terp.tObj.addMethod(new JavMeth(terp, "ifNil:", "ifn:") {
                public Pro apply(Frame f, Pro r, Pro[] args) {
                    if (r.asNil() != null) {
                        Blk blk = args[0].asBlk();
                        return blk.body.eval(f);
                    } else {
                        return terp.instNil;
                    }
                }
            });
            terp.tObj.addMethod(new JavMeth(terp, "ifNotNil:", "ifnn:") {
                public Pro apply(Frame f, Pro r, Pro[] args) {
                    if (r.asNil() == null) {
                        Blk blk = args[0].asBlk();
                        return blk.body.eval(f);
                    } else {
                        return terp.instNil;
                    }
                }
            });
            terp.tObj.addMethod(new JavMeth(terp, "ifNil:ifNotNil:", "ifn:ifnn:") {
                public Pro apply(Frame f, Pro r, Pro[] args) {
                    Blk blk = args[asNil() == null ? 0 : 1].asBlk();
                    return blk.body.eval(f);
                }
            });
            terp.tObj.addMethod(new JavMeth(terp, "ifNotNil:ifNil:", "ifnn:ifn:") {
                public Pro apply(Frame f, Pro r, Pro[] args) {
                    Blk blk = args[asNil() == null ? 1 : 0].asBlk();
                    return blk.body.eval(f);
                }
            });
            terp.tObj.addMethod(new JavMeth(terp, "ifYes:", "y:") {
                public Pro apply(Frame f, Pro r, Pro[] args) {
                    if (r.truth()) {
                        Blk blk = args[0].asBlk();
                        return blk.evalWithoutArgs();
                    } else {
                        return terp.instNil;
                    }
                }
            });
            terp.tObj.addMethod(new JavMeth(terp, "ifNo:", "n:") {
                public Pro apply(Frame f, Pro r, Pro[] args) {
                    if (r.truth()) {
                        return terp.instNil;
                    } else {
                        Blk blk = args[0].asBlk();
                        return blk.evalWithoutArgs();
                    }
                }
            });
            terp.tObj.addMethod(new JavMeth(terp, "ifYes:ifNo:", "y:n:") {
                public Pro apply(Frame f, Pro r, Pro[] args) {
                    Blk blk = args[r.truth() ? 0 : 1].asBlk();
                    return blk.body.eval(f);
                }
            });
            terp.tObj.addMethod(new JavMeth(terp, "ifNo:ifYes:", "n:y:") {
                public Pro apply(Frame f, Pro r, Pro[] args) {
                    Blk blk = args[r.truth() ? 1 : 0].asBlk();
                    return blk.body.eval(f);
                }
            });
            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, "eval:", "") {
                public Pro apply(Frame f, Pro r, Pro[] args) {
                    assert args.length == 1;
                    String code = args[0].asStr().str;
                    Pro z = r.asObj().eval(code);
                    say("EVAL -> %s", z);
                    return z;
                }
            });
            terp.tObj.addMethod(new JavMeth(terp, "isInstanceOf:", "isa:") {
                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", "is") {
                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, "allClasses", "clss") {
                public Pro apply(Frame f, Pro r, Pro[] args) {
                    assert args.length == 0;
                    Pro[] arr = new Pro[terp.clss.size() * 2];
                    int i = 0;
                    for (String k : terp.clss.keySet()) {
                        arr[i] = terp.newStr(k);
                        arr[i + 1] = terp.clss.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);
        }
        // Most objects are true.
        // nil and Nums that round to 0 are false.
        boolean truth() {
            return false;
        }
        static void addBuiltinMethodsForFal(final Terp terp) {
        }
    }

    public final static class Blk extends Obj {
        Expr.Block block;
        Expr body;
        String[] params;
        Frame f;

        public Blk(Expr.Block block, Frame f) {
            super(f.terp().tBlk);
            this.block = block;
            this.body = block.body;
            this.params = block.params;
            this.f = f;
        }
        Blk asBlk() {
            return this;
        }
        public String toString() {
            return block.toString();
        }
        Pro evalWithoutArgs() {  // Cannot just call it eval(), because that names takes a Frame, and this does not.
            if ( this.params.length != 0 ) {
                toss("Block takes <%d> params, but calling it with 0 arsg: Blk=<%s>", this.params.length, this);
            }
            // Notice the block runs in its frame, not the caller's.
            return this.body.eval(this.f);
        }
        Pro evalWith1Arg(Pro arg0) {
            if ( this.params.length != 1 ) {
                toss("Block takes <%d> params, but calling it with 1 arg: Blk=<%s> arg0=<%s>", this.params.length, this, arg0);
            }
            this.f.dict.put(params[0].toLowerCase(), arg0);
            // Notice the block runs in its frame, not the caller's.
            return this.body.eval(this.f);
        }
        Pro evalWith2Args(Pro arg0, Pro arg1) {
            if ( this.params.length != 2 ) {
                toss("Block takes <%d> params, but calling it with 2 args: Blk=<%s> arg0=<%s> arg1=<%s>", this.params.length, this, arg0, arg1);
            }
            this.f.dict.put(params[0].toLowerCase(), arg0);
            this.f.dict.put(params[1].toLowerCase(), arg1);
            // Notice the block runs in its frame, not the caller's.
            return this.body.eval(this.f);
        }

        static void addBuiltinMethodsForBlk(final Terp terp) {

            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;
                }
            });
            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) {
                    return r.asBlk().evalWithoutArgs();
                }
            });
            terp.tBlk.addMethod(new JavMeth(terp, "run:", "r:", "Run the block with one argument.") {
                public Pro apply(Frame _, Pro r, Pro[] args) {
                    return r.asBlk().evalWith1Arg(args[0]);
                }
            });
            terp.tBlk.addMethod(new JavMeth(terp, "run:with:", "r:wi:", "Run the block with two arguments.") {
                public Pro apply(Frame _, Pro r, Pro[] args) {
                    return r.asBlk().evalWith2Args(args[0], args[1]);
                }
            });
        }
    }

    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);
        }
        // Most objects are true.
        // nil and Nums that round to 0 are false.
        boolean truth() {
            return toNearestInt() != 0;
        }

        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 boolObj(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(final 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.evalWith1Arg(new Num(f.terp(), i));
                    }
                    return f.terp().instNil;
                }
            });

            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 boolObj(compute(s.str, a.str));
            }
            abstract boolean compute(String sstr, String astr);
        }

        static void addBuiltinMethodsForStr(final Terp terp) {
            terp.tStr.addMethod(new JavMeth(terp, "split:", "", "Split 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(Pattern.quote(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());
                }
            });
            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);
        }
        public boolean equals(Object o) {
            if (o instanceof Vec) {
                Vec that = (Vec)o;
                int sz = this.vec.size();
                if (sz == that.vec.size()) {
                    for (int i= 0; i < sz; i++) {
                        if (!this.vec.get(i).equals(that.vec.get(i))) {
                            return false;
                        }
                    }
                    return true;
                }
            }
            return false;
        }
        public int hashCode() {
            int z = 0;
            int sz = this.vec.size();
            for (int i= 0; i < sz; i++) {
                z = 13 * z + this.vec.get(i).hashCode();
            }
            return z;
        }
        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:put:", "at:p:", "") {
                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.evalWith1Arg(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 {
        HashMap<Pro, Pro> map;

        Map(Terp t) {
            super(t.tMap);
            this.map = new HashMap<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 boolean equals(Object o) {
            if (o instanceof Map) {
                Map that = (Map)o;
                int sz = this.map.size();
                if (sz == that.map.size()) {
                    Assoc[] a = this.sortedAssocs();
                    Assoc[] b = that.sortedAssocs();
                    for (int i= 0; i < sz; i++) {
                        if (!a[i].key.equals(b[i].key) || !a[i].value.equals(b[i].value)) {
                            return false;
                        }
                    }
                    return true;
                }
            }
            return false;
        }
        public int hashCode() {
            int z = 0;
            Assoc[] a = this.sortedAssocs();
            for (int i= 0; i < a.length; i++) {
                z = 13 * z + a[i].key.hashCode();
                z = 13 * z + a[i].value.hashCode();
            }
            return z;
        }
        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:put:", "at:p:", "") {
                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.
            // TODO: that's too strange.
            return key;
        }
    }

}
