package terse.talk;

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

import terse.talk.Cls.JavaMeth;
import terse.talk.Cls.StrMeth;
import terse.talk.Expr.Seq;
import terse.talk.Talk.Terp.Frame;

public class Pro extends Talk implements Comparable<Pro> {
    public 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;
    }
    Dict asDict() {
        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();
        }
    }
    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.
                boolean z = innerThis.equals(innerThat);
                return z;
            } else {
                // Avoid infinite recursion.
                boolean z = this == that;
                return z;
            }
        } 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 JavaMeth(terp.tPro, "class", "cls") {
            @Override
            public Pro apply(Frame f, Pro r, Pro[] args) {
                say("<Pro class> r=%s r.cls=%s r.cls.name=%s r.cls.name=%s", r, r.cls, r.cls.name, r.cls.name);
                return r.cls;
            }
        });
    }

    /** Special marker object, for message to super. */
    public static class Super extends Pro {
        Super(Terp terp) {
            super(terp.tSuper);
        }
        public String toString() {
            return "super ";
        }
    }

    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;
        }
        Pro eval(String code, Pro a) {
//            say("EVAL <%s> <<< <%s> <%s>", this, code, a);
            Expr.Top top = Parser.parseMethod(cls, "__eval__:", code);
            Frame f = cls.terp.newFrame(null, this, top);
            f.locals[0] = a;
            Pro z = top.eval(f);
            assert z != null : fmt("Null result in %s.eval <%s>", this, code);
//            say("EVAL <%s> >>> <%s>", this, z);
            return z;
        }
        Pro eval(String code, Pro a, Pro b) {
//            say("EVAL <%s> <<< <%s> <%s> <%s>", this, code, a, b);
            Expr.Top top = Parser.parseMethod(cls, "__eval__:__eval__:", code);
            Frame f = cls.terp.newFrame(null, this, top);
            f.locals[0] = a;
            f.locals[1] = b;
            Pro z = top.eval(f);
            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 JavaMeth(terp.tObj, "must", "") {
                public Pro apply(Frame f, Pro r, Pro[] args) {
                    if (!r.truth()) {
                        toss("MUST be True, but isn't: %s", r);
                    }
                    return r;
                }
            });
            terp.tObj.addMethod(new JavaMeth(terp.tObj, "must:", "") {
                public Pro apply(Frame f, Pro r, Pro[] args) {
                    if (!r.truth()) {
                        StringBuffer sb = new StringBuffer();
                        if (args[0].cls == terp.tBlk) {
                            Pro x = ((Blk)args[0]).eval("self vec");
                            sb.append("; ");
                            sb.append(x.toString());
                        }
                        toss("MUST be True, but isn't: %s %s", r, sb);
                    }
                    return r;
                }
            });
            terp.tObj.addMethod(new JavaMeth(terp.tObj, "cant:", "") {
                public Pro apply(Frame f, Pro r, Pro[] args) {
                    if (r.truth()) {
                        StringBuffer sb = new StringBuffer();
                        if (args[0].cls == terp.tBlk) {
                            Pro x = ((Blk)args[0]).eval("self vec");
                            sb.append("; ");
                            sb.append(x.toString());
                        }
                        toss("CANT be True, but is: %s %s", r, sb);
                    }
                    return r;
                }
            });
            terp.tObj.addMethod(new JavaMeth(terp.tObj, "err:", "") {
                public Pro apply(Frame f, Pro r, Pro[] args) {
                    toss("ERROR: %s: %s", r, args[0]);
                    return r;
                }
            });
            terp.tObj.addMethod(new JavaMeth(terp.tObj, "err", "") {
                public Pro apply(Frame f, Pro r, Pro[] args) {
                    toss("ERROR: %s", r);
                    return r;
                }
            });
            terp.tObj.addMethod(new JavaMeth(terp.tObj, "log", "") {
                public Pro apply(Frame f, Pro r, Pro[] args) {
                    say("#LOG# %s", r);
                    return r;
                }
            });
            terp.tObj.addMethod(new JavaMeth(terp.tObj, "log:", "") {
                public Pro apply(Frame f, Pro r, Pro[] args) {
                    say("#LOG# %s; %s", r, args[0]);
                    return r;
                }
            });
            terp.tObj.addMethod(new JavaMeth(terp.tObj, "hash", "") {
                public Pro apply(Frame f, Pro r, Pro[] args) {
                    return terp.newNum(System.identityHashCode(r.innerObject()));
                }
            });
            terp.tObj.addMethod(new JavaMeth(terp.tObj, "not", "") {
                public Pro apply(Frame f, Pro r, Pro[] args) {
                    return boolObj(r.truth() == false);
                }
            });
            terp.tObj.addMethod(new JavaMeth(terp.tObj, "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 JavaMeth(terp.tObj, "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 JavaMeth(terp.tObj, "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 JavaMeth(terp.tObj, "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 JavaMeth(terp.tObj, "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 JavaMeth(terp.tObj, "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 JavaMeth(terp.tObj, "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 JavaMeth(terp.tObj, "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 JavaMeth(terp.tObj, "str", "") {
                public Pro apply(Frame f, Pro r, Pro[] args) {
                    assert args.length == 0;
                    return terp.newStr(r.toString());
                }
            });
            terp.tObj.addMethod(new JavaMeth(terp.tObj, "eval:", "") {
                public Pro apply(Frame f, Pro r, Pro[] args) {
                    assert args.length == 1;
                    Str code = args[0].asStr();
                    if (code == null) {
                        say("Expected a Str arg to eval: but got this instead: <%s: %s>", args[0].cls, args[0]);
                    }
                    Pro z = r.asObj().eval(code.str);
//                    say("EVAL -> %s", z);
                    return z;
                }
            });
            terp.tObj.addMethod(new JavaMeth(terp.tObj, "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 JavaMeth(terp.tObj, "equals:", "") {
                public Obj apply(Frame f, Pro r, Pro[] args) {
                    assert args.length == 1;
                    return r.equals(args[0]) ? terp.instTrue : terp.instFalse;
                }
            });
            terp.tObj.addMethod(new JavaMeth(terp.tObj, "isSameAs:", "is:") {
                public Obj apply(Frame f, Pro r, Pro[] args) {
                    assert args.length == 1;
                    return r == 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 StrMeth(terp.tSysCls, "envAt:", "", "") {
                public Pro apply1(Frame f, Pro r, Str a) {
                    Pro z = terp.newStr(terp.env.get(a.str));
                    return z == null ? terp.instNil : z;
                }
            });
        }
    }

    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 own frame, not the caller's frame.
            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.locals[block.paramsLocalIndex[0]] = arg0;
            // Notice the block runs in its own frame, not the caller's frame.
            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.locals[block.paramsLocalIndex[0]] = arg0;
            this.f.locals[block.paramsLocalIndex[1]] = 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 JavaMeth(terp.tBlk, "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 JavaMeth(terp.tBlk, "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 JavaMeth(terp.tBlk, "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 JavaMeth(terp.tBlk, "dict", "d", "Evalute pairs of the block to make a Dict."
                    ) {
                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);  // should be a pair.
                    }
                    return terp.newDict(arr);
                }
            });
            terp.tBlk.addMethod(new JavaMeth(terp.tBlk, "run", "r", "Evaluate the block.") {
                public Pro apply(Frame _, Pro r, Pro[] args) {
                    return r.asBlk().evalWithoutArgs();
                }
            });
            terp.tBlk.addMethod(new JavaMeth(terp.tBlk, "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 JavaMeth(terp.tBlk, "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]);
                }
            });
            terp.tBlk.addMethod(new JavaMeth(terp.tBlk, "catch", "", "Catch exceptions.") {
                public Pro apply(Frame _, Pro r, Pro[] args) {
                    Pro z = terp.instNil;
                    Pro err = terp.instNil;
                    Pro info = terp.instNil;
                    try {
                        z = r.asBlk().evalWithoutArgs();
                    } catch (RuntimeException ex) {
                        err = terp.newStr(ex.toString());
                        StackTraceElement[] t = ex.getStackTrace();
                        StringBuffer sb = new StringBuffer();
                        for (int i = 0; i < t.length; i++) {
                            sb.append(t[i].toString());
                            sb.append("\n");
                        }
                        info = terp.newStr(sb.toString());
                    }
                    return terp.newVec(pros(z, err, info));
                }
            });
        }
    }

    public final static class Num extends Obj {
        public double num;

        Num(Terp t, double num) {
            super(t.tNum);
            this.num = num;
        }
        Num asNum() {
            return this;
        }
        public String toString() {
            long truncated = (long) num;
            if (num == truncated) {
                return fmt("%d", truncated);
            } else {
                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 JavaMeth {
            BinaryNumPredMeth(Terp terp, String name) {
                super(terp.tNum, 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 JavaMeth {
            BinaryNumMeth(Terp terp, String name) {
                super(terp.tNum, 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 JavaMeth {
            UnaryNumMeth(Terp terp, String name) {
                super(terp.tNum, 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 JavaMeth(terp.tNum, "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;
                }
            });
            terp.tNum.addMethod(new UnaryNumMeth(terp, "floor") {
                double compute(double snum) {
                    return Math.floor(snum);
                }
            });
            terp.tNum.addMethod(new UnaryNumMeth(terp, "abs") {
                double compute(double snum) {
                    return Math.abs(snum);
                }
            });
            terp.tNumCls.addMethod(new JavaMeth(terp.tNumCls, "e", "", "e, the base of natural logs") {
                public Pro apply(Frame f, Pro r, Pro[] args) {
                    return terp.newNum(Math.E);
                }
            });
            terp.tNumCls.addMethod(new JavaMeth(terp.tNumCls, "pi", "", "pi") {
                public Pro apply(Frame f, Pro r, Pro[] args) {
                    return terp.newNum(Math.PI);
                }
            });
            terp.tNum.addMethod(new UnaryNumMeth(terp, "sin") {
                double compute(double snum) {
                    return Math.sin(snum);
                }
            });
            terp.tNum.addMethod(new UnaryNumMeth(terp, "cos") {
                double compute(double snum) {
                    return Math.cos(snum);
                }
            });
            terp.tNum.addMethod(new UnaryNumMeth(terp, "tan") {
                double compute(double snum) {
                    return Math.tan(snum);
                }
            });
            terp.tNum.addMethod(new UnaryNumMeth(terp, "asin") {
                double compute(double snum) {
                    return Math.asin(snum);
                }
            });
            terp.tNum.addMethod(new UnaryNumMeth(terp, "acos") {
                double compute(double snum) {
                    return Math.acos(snum);
                }
            });
            terp.tNum.addMethod(new UnaryNumMeth(terp, "atan") {
                double compute(double snum) {
                    return Math.atan(snum);
                }
            });
            terp.tNum.addMethod(new UnaryNumMeth(terp, "log") {
                double compute(double snum) {
                    return Math.log(snum);
                }
            });
            terp.tNum.addMethod(new UnaryNumMeth(terp, "log10") {
                double compute(double snum) {
                    return Math.log10(snum);
                }
            });

            terp.tNum.addMethod(new UnaryNumMeth(terp, "exp") {
                double compute(double snum) {
                    return Math.exp(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 JavaMeth(terp.tBuf, "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 JavaMeth(terp.tBuf, "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 JavaMeth(terp.tBufCls, "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 JavaMeth(terp.tBufCls, "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 {
        public String str;

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

        abstract static class BinaryStrPredMeth extends JavaMeth {
            BinaryStrPredMeth(Terp terp, String name) {
                super(terp.tStr, name, null, "");
            }
            public Pro apply(Frame f, Pro r, Pro[] args) {
                assert args.length == 1;
                Str s = (Str) r;
                Str a = args[0].asStr();
                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 JavaMeth(terp.tStr, "split:", "", "Split string into a Vec, using arg as delimiter.") {
                        public Pro apply(Frame f, Pro r, Pro[] args) {
                            assert args.length == 1;
                            Str s = (Str) r;
                            Str a = args[0].asStr();
                            if (a == null) {
                                toss("Argument of Str>>spli: not a Str");
                            }
                            // Using Java's split caused infinite recursion in
                            // Pattern:
                            // String[] parts =
                            // s.str.split(Pattern.quote(a.str));
                            String[] parts = splitNonEmpty(s.str, a.str.charAt(0));
                            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 JavaMeth(terp.tStr, "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 = (Str) r;
                    Str a = args[0].asStr();
                    if (a == null) {
                        toss("Argument of Str>>ap: not a Str");
                    }
                    return new Str(terp, s.str + a.str);
                }
            });
            terp.tStr.addMethod(new JavaMeth(terp.tStr, "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 = (Str) r;
                            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 JavaMeth(terp.tStr, "head", "hd", "First char of Str.") {
                public Pro apply(Frame f, Pro r, Pro[] args) {
                    Str self = (Str) r;
                    if (self.str.length() == 0) {
                        toss("Cannot take head of empty Str");
                    }
                    return terp.newStr(self.str.substring(0, 1));
                }
            });
            terp.tStr.addMethod(new JavaMeth(terp.tStr, "tail", "tl", "All but first char of Str.") {
                public Pro apply(Frame f, Pro r, Pro[] args) {
                    Str self = (Str) r;
                    if (self.str.length() == 0) {
                        toss("Cannot take tail of empty Str");
                    }
                    return terp.newStr(self.str.substring(1));
                }
            });
            terp.tStr.addMethod(new JavaMeth(terp.tStr, "length", "len", "Length of Str.") {
                public Pro apply(Frame f, Pro r, Pro[] args) {
                    return terp.newNum(((Str) r).str.length());
                }
            });
            terp.tStr.addMethod(new JavaMeth(terp.tStr, "toNumber", "num", "Convert ASCII Str to Num") {
                public Pro apply(Frame f, Pro r, Pro[] args) {
                    Str self = (Str) r;
                    double x = 0;
                    try {
                        x = new Float(self.str);
                    } catch (NumberFormatException ex) {
                        toss("Cannot convert Str to Num: <%s>", self.str);
                    }
                    return terp.newNum(x);
                }
            });
            terp.tStr.addMethod(new JavaMeth(terp.tStr, "toLower", "low", "Convert string to lowercase.") {
                public Pro apply(Frame f, Pro r, Pro[] args) {
                    return terp.newStr(((Str) r).str.toLowerCase());
                }
            });
            terp.tStr.addMethod(new JavaMeth(terp.tStr, "toUpper", "upp", "Convert string to uppercase.") {
                public Pro apply(Frame f, Pro r, Pro[] args) {
                    return terp.newStr(((Str) r).str.toUpperCase());
                }
            });
            terp.tStr.addMethod(new JavaMeth(terp.tStr, "trimWhite", "trimw", "Trim whitespace from front and back.") {
                public Pro apply(Frame f, Pro r, Pro[] args) {
                    return terp.newStr(((Str) r).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 {
        public Vector<Pro> vec;

        Vec(Terp t) {
            super(t.tVec);
            this.vec = new Vector<Pro>();
        }
        Vec(Pro[] nonemptyArray) {
            super(nonemptyArray[0].cls.terp.tVec);
            this.vec = new Vector<Pro>();
            for (int i = 0; i < nonemptyArray.length; i++) {
                this.vec.add(nonemptyArray[i]);
            }
        }
        Vec asVec() {
            return this;
        }
        public String toString() {
            StringBuffer sb = new StringBuffer("Vec{");
            for (int i = 0; i < vec.size(); i++) {
                sb.append(vec.get(i) == null ? " nil\"NULL\" " : vec.get(i).toString());
                sb.append("; ");
            }
            sb.append("} ");
            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 compareTo(Pro 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++) {
                        int z = this.vec.get(i).compareTo(that.vec.get(i));
                        if (z != 0) {
                            return z;
                        }
                    }
                    return 0;
                } else {
                    return new Integer(this.vec.size()).compareTo(new Integer(that.vec.size()));
                }
            } else if (o instanceof Pro) {
                // Within our Pro world, order by classname, providing stability after filein/fileout.
                Pro that = (Pro) o;
                return this.cls.name.compareTo(that.cls.name);
            } else {
                // Against foreign objects, fall back to identity hash.
                return new Integer(System.identityHashCode(this)).compareTo(new Integer(System.identityHashCode(o)));
            }
        }
        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 JavaMeth(terp.tVec, "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 JavaMeth(terp.tVec, "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 JavaMeth(terp.tVec, "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 JavaMeth(terp.tVec, "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 JavaMeth(terp.tVec, "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 JavaMeth(terp.tVec, "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 JavaMeth(terp.tVecCls, "new", "", "Create a new empty Vec object.") {
                public Pro apply(Frame f, Pro r, Pro[] args) {
                    return new Vec(terp);
                }
            });
            terp.tVecCls.addMethod(new JavaMeth(terp.tVecCls, "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 Dict extends Obj {
        public HashMap<Pro, Pro> dict;

        Dict(Terp t) {
            super(t.tDict);
            this.dict = new HashMap<Pro, Pro>();
        }
        Dict asDict() {
            return this;
        }
        public String toString() {
            Vec[] aarr = sortedAssocs();
            StringBuffer sb = new StringBuffer("Dict{");
            for (Vec a : aarr) {
                sb.append("(");
                sb.append(a.vec.get(0).toString());
                sb.append(", ");
                sb.append(a.vec.get(1).toString());
                sb.append("). ");
            }
            sb.append("} ");
            return sb.toString();
        }
        public boolean equals(Object o) {
            if (o instanceof Dict) {
                Dict that = (Dict) o;
                int sz = this.dict.size();
                if (sz == that.dict.size()) {
                    Vec[] a = this.sortedAssocs();
                    Vec[] b = that.sortedAssocs();
                    for (int i = 0; i < sz; i++) {
                        if (!a[i].vec.get(0).equals(b[i].vec.get(0)) || !a[i].vec.get(1).equals(b[i].vec.get(1))) {
                            return false;
                        }
                    }
                    return true;
                }
            }
            return false;
        }
        public int hashCode() {
            int z = 0;
            Vec[] a = this.sortedAssocs();
            for (int i = 0; i < a.length; i++) {
                z = 13 * z + a[i].vec.get(0).hashCode();
                z = 13 * z + a[i].vec.get(1).hashCode();
            }
            return z;
        }
        public void visit(Visitor v) {
            v.visitDict(this);
        }
        Vec[] sortedAssocs() {
            Vec[] z = new Vec[dict.size()];
            int i = 0;
            for (Pro key : dict.keySet()) {
                z[i] = new Vec(pros(key, dict.get(key)));
                ++i;
            }
            Arrays.sort(z);
            return z;
        }

        static void addBuiltinMethodsForDict(final Terp terp) {
            terp.tDict.addMethod(new JavaMeth(terp.tDict, "len", "", "Number of entries in the Dict.") {
                public Num apply(Frame f, Pro r, Pro[] args) {
                    return new Num(terp, r.asDict().dict.size());
                }
            });
            terp.tDict.addMethod(new JavaMeth(terp.tDict, "dir", "", "Number of entries in the Dict.") {
                public Vec apply(Frame f, Pro r, Pro[] args) {
                    Vec z = terp.newVec(new Vec[0]);
                    for (Vec aa : r.asDict().sortedAssocs()) {
                        z.vec.add(aa.vec.get(0));
                    }
                    return z;
                }
            });
            terp.tDict.addMethod(new JavaMeth(terp.tDict, "at:", "",
                    "Return the value stored at the given key, or Nil if it is missing.") {
                public Pro apply(Frame f, Pro r, Pro[] args) {
                    Dict self = r.asDict();
                    Pro z = self.dict.get(args[0]);
                    if (z == null) {
                        return terp.instNil;
                    }
                    return z;
                }
            });
            terp.tDict.addMethod(new JavaMeth(terp.tDict, "at:put:", "at:p:", "") {
                public Pro apply(Frame f, Pro r, Pro[] args) {
                    Dict self = r.asDict();
                    self.dict.put(args[0], args[1]);
                    return r;
                }
            });
            terp.tDictCls.addMethod(new JavaMeth(terp.tDictCls, "new", "", "Create a new empty Dict object.") {
                public Pro apply(Frame f, Pro r, Pro[] args) {
                    return new Dict(terp);
                }
            });
        }
    }
}
